summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-11-21 22:06:11 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-11-21 22:06:11 +0800
commitb20c7846ffc0c78d6b483184363b6ccc7c11326d (patch)
tree05d232a4f877cd94481afcb8bf912410b58bbd0b
parentf593abbc70ab02823cd99d2db11598b629cbb3a0 (diff)
parent77a4382712888131813ec008a8d4179bcf753b8b (diff)
downloadgitlab-ce-b20c7846ffc0c78d6b483184363b6ccc7c11326d.tar.gz
Merge remote-tracking branch 'upstream/master' into show-commit-status-from-latest-pipeline
* upstream/master: (754 commits) Add documentation about todos and failed builds Add Changelog entry for failed builds todos fix Do not create TODO when build is allowed to fail Fix 404 on some group pages when name contains dot Grapify the users API Remove duplicate sidekiq throttling parameters Fix regression in Merge request form Fix wrong link Fix test Fix broken test Changes for stop url to path fix typo in gitlab_flow.md ('munch'->'much') Fix spec Backport some changes done from Time Tracking feature in EE. Use build instead create in BroadcastMessage model spec Try to fix tests Remove unnecessary self from user model Expose stop_path for environment to not construct that in frontend Bring back the `commit_url` as it's used by CycleAnalytics Add api endpoint for creating a pipeline ...
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab-ci.yml52
-rw-r--r--CHANGELOG.md51
-rw-r--r--CONTRIBUTING.md11
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock53
-rw-r--r--README.md4
-rw-r--r--app/assets/javascripts/activities.js6
-rw-r--r--app/assets/javascripts/admin.js2
-rw-r--r--app/assets/javascripts/api.js2
-rw-r--r--app/assets/javascripts/application.js29
-rw-r--r--app/assets/javascripts/aside.js2
-rw-r--r--app/assets/javascripts/autosave.js2
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/behaviors/autosize.js2
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js2
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js2
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js2
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js2
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es612
-rw-r--r--app/assets/javascripts/boards/components/board.js.es615
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es62
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es636
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es618
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es62
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es699
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es63
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es64
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js2
-rw-r--r--app/assets/javascripts/breakpoints.js2
-rw-r--r--app/assets/javascripts/broadcast_message.js2
-rw-r--r--app/assets/javascripts/build.js101
-rw-r--r--app/assets/javascripts/build_artifacts.js2
-rw-r--r--app/assets/javascripts/commit.js2
-rw-r--r--app/assets/javascripts/commit/file.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js2
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/compare.js5
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js2
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/diff.js6
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es614
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es614
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es63
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es643
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es6248
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es667
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es622
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es6494
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es631
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es627
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es621
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es622
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es6131
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js2
-rw-r--r--app/assets/javascripts/extensions/jquery.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js2
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es615
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/gl_form.js2
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js2
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js2
-rw-r--r--app/assets/javascripts/group_avatar.js2
-rw-r--r--app/assets/javascripts/group_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/groups_select.js2
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/issuable.js.es622
-rw-r--r--app/assets/javascripts/issuable_context.js2
-rw-r--r--app/assets/javascripts/issuable_form.js2
-rw-r--r--app/assets/javascripts/issue.js2
-rw-r--r--app/assets/javascripts/issue_status_select.js2
-rw-r--r--app/assets/javascripts/labels.js2
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/layout_nav.js2
-rw-r--r--app/assets/javascripts/lib/chart.js2
-rw-r--r--app/assets/javascripts/lib/cropper.js2
-rw-r--r--app/assets/javascripts/lib/d3.js2
-rw-r--r--app/assets/javascripts/lib/raphael.js2
-rw-r--r--app/assets/javascripts/lib/utils/animate.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js97
-rw-r--r--app/assets/javascripts/lib/utils/notify.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/timeago.js239
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/line_highlighter.js2
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/member_expiration_date.js2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es62
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es615
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es620
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es63
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js11
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es62
-rw-r--r--app/assets/javascripts/merged_buttons.js2
-rw-r--r--app/assets/javascripts/milestone.js2
-rw-r--r--app/assets/javascripts/milestone_select.js4
-rw-r--r--app/assets/javascripts/namespace_select.js2
-rw-r--r--app/assets/javascripts/network/branch_graph.js2
-rw-r--r--app/assets/javascripts/network/network.js2
-rw-r--r--app/assets/javascripts/network/network_bundle.js4
-rw-r--r--app/assets/javascripts/new_branch_form.js2
-rw-r--r--app/assets/javascripts/new_commit_form.js2
-rw-r--r--app/assets/javascripts/notes.js80
-rw-r--r--app/assets/javascripts/notifications_dropdown.js2
-rw-r--r--app/assets/javascripts/notifications_form.js2
-rw-r--r--app/assets/javascripts/pager.js2
-rw-r--r--app/assets/javascripts/pipelines.js.es618
-rw-r--r--app/assets/javascripts/preview_markdown.js2
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js2
-rw-r--r--app/assets/javascripts/project.js2
-rw-r--r--app/assets/javascripts/project_avatar.js2
-rw-r--r--app/assets/javascripts/project_find_file.js2
-rw-r--r--app/assets/javascripts/project_fork.js2
-rw-r--r--app/assets/javascripts/project_import.js2
-rw-r--r--app/assets/javascripts/project_label_subscription.js.es653
-rw-r--r--app/assets/javascripts/project_new.js2
-rw-r--r--app/assets/javascripts/project_select.js2
-rw-r--r--app/assets/javascripts/project_show.js2
-rw-r--r--app/assets/javascripts/projects_list.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/shortcuts.js2
-rw-r--r--app/assets/javascripts/shortcuts_blob.js2
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js2
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js2
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/shortcuts_network.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js14
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js2
-rw-r--r--app/assets/javascripts/star.js2
-rw-r--r--app/assets/javascripts/subscription.js2
-rw-r--r--app/assets/javascripts/subscription_select.js2
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/tree.js2
-rw-r--r--app/assets/javascripts/u2f/authenticate.js2
-rw-r--r--app/assets/javascripts/u2f/error.js2
-rw-r--r--app/assets/javascripts/u2f/register.js2
-rw-r--r--app/assets/javascripts/u2f/util.js2
-rw-r--r--app/assets/javascripts/users/calendar.js2
-rw-r--r--app/assets/javascripts/users/users_bundle.js2
-rw-r--r--app/assets/javascripts/users_select.js2
-rw-r--r--app/assets/javascripts/vue_common_component/commit.js.es6176
-rw-r--r--app/assets/javascripts/wikis.js2
-rw-r--r--app/assets/javascripts/zen_mode.js2
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss7
-rw-r--r--app/assets/stylesheets/framework/buttons.scss15
-rw-r--r--app/assets/stylesheets/framework/common.scss16
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/header.scss23
-rw-r--r--app/assets/stylesheets/framework/highlight.scss4
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss25
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/page-header.scss67
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/framework/variables.scss10
-rw-r--r--app/assets/stylesheets/framework/wells.scss45
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/boards.scss8
-rw-r--r--app/assets/stylesheets/pages/builds.scss51
-rw-r--r--app/assets/stylesheets/pages/commit.scss142
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss33
-rw-r--r--app/assets/stylesheets/pages/environments.scss43
-rw-r--r--app/assets/stylesheets/pages/groups.scss25
-rw-r--r--app/assets/stylesheets/pages/issuable.scss14
-rw-r--r--app/assets/stylesheets/pages/issues.scss25
-rw-r--r--app/assets/stylesheets/pages/labels.scss10
-rw-r--r--app/assets/stylesheets/pages/login.scss23
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss112
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss7
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss58
-rw-r--r--app/assets/stylesheets/pages/search.scss14
-rw-r--r--app/controllers/admin/application_settings_controller.rb5
-rw-r--r--app/controllers/autocomplete_controller.rb8
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb7
-rw-r--r--app/controllers/concerns/diff_for_path.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issues_action.rb1
-rw-r--r--app/controllers/concerns/merge_requests_action.rb1
-rw-r--r--app/controllers/concerns/toggle_subscription_action.rb6
-rw-r--r--app/controllers/groups/labels_controller.rb7
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/help_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb4
-rw-r--r--app/controllers/profiles/chat_names_controller.rb64
-rw-r--r--app/controllers/profiles_controller.rb10
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb9
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb65
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb11
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/forks_controller.rb1
-rw-r--r--app/controllers/projects/git_http_client_controller.rb8
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/labels_controller.rb7
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/network_controller.rb14
-rw-r--r--app/controllers/projects/notes_controller.rb25
-rw-r--r--app/controllers/projects/pipelines_controller.rb4
-rw-r--r--app/controllers/projects/services_controller.rb7
-rw-r--r--app/controllers/projects_controller.rb65
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb2
-rw-r--r--app/controllers/users_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb33
-rw-r--r--app/finders/labels_finder.rb47
-rw-r--r--app/helpers/accounts_helper.rb5
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb10
-rw-r--r--app/helpers/components_helper.rb9
-rw-r--r--app/helpers/diff_helper.rb7
-rw-r--r--app/helpers/environment_helper.rb29
-rw-r--r--app/helpers/environments_helper.rb7
-rw-r--r--app/helpers/events_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb22
-rw-r--r--app/helpers/labels_helper.rb30
-rw-r--r--app/helpers/lfs_helper.rb8
-rw-r--r--app/helpers/notifications_helper.rb9
-rw-r--r--app/helpers/preferences_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb7
-rw-r--r--app/helpers/search_helper.rb29
-rw-r--r--app/helpers/services_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb4
-rw-r--r--app/helpers/triggers_helper.rb12
-rw-r--r--app/mailers/emails/pipelines.rb17
-rw-r--r--app/models/application_setting.rb21
-rw-r--r--app/models/chat_name.rb12
-rw-r--r--app/models/ci/build.rb41
-rw-r--r--app/models/ci/pipeline.rb11
-rw-r--r--app/models/concerns/issuable.rb24
-rw-r--r--app/models/concerns/select_for_project_authorization.rb9
-rw-r--r--app/models/concerns/subscribable.rb64
-rw-r--r--app/models/concerns/token_authenticatable.rb10
-rw-r--r--app/models/cycle_analytics.rb64
-rw-r--r--app/models/environment.rb5
-rw-r--r--app/models/event.rb9
-rw-r--r--app/models/external_issue.rb9
-rw-r--r--app/models/group.rb13
-rw-r--r--app/models/guest.rb7
-rw-r--r--app/models/issue.rb54
-rw-r--r--app/models/issue_collection.rb42
-rw-r--r--app/models/key.rb7
-rw-r--r--app/models/member.rb13
-rw-r--r--app/models/merge_request.rb15
-rw-r--r--app/models/merge_request/metrics.rb1
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/notification_setting.rb4
-rw-r--r--app/models/project.rb135
-rw-r--r--app/models/project_authorization.rb8
-rw-r--r--app/models/project_feature.rb24
-rw-r--r--app/models/project_group_link.rb7
-rw-r--r--app/models/project_services/chat_service.rb21
-rw-r--r--app/models/project_services/jira_service.rb171
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb56
-rw-r--r--app/models/project_services/pipelines_email_service.rb20
-rw-r--r--app/models/project_services/slack_service/note_message.rb12
-rw-r--r--app/models/project_services/slack_service/pipeline_message.rb7
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/repository.rb56
-rw-r--r--app/models/service.rb8
-rw-r--r--app/models/subscription.rb7
-rw-r--r--app/models/tree.rb22
-rw-r--r--app/models/user.rb198
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/ci/pipeline_policy.rb4
-rw-r--r--app/policies/issuable_policy.rb2
-rw-r--r--app/policies/issue_policy.rb13
-rw-r--r--app/serializers/analytics_build_entity.rb40
-rw-r--r--app/serializers/analytics_build_serializer.rb3
-rw-r--r--app/serializers/analytics_commit_entity.rb13
-rw-r--r--app/serializers/analytics_commit_serializer.rb3
-rw-r--r--app/serializers/analytics_generic_serializer.rb7
-rw-r--r--app/serializers/analytics_issue_entity.rb29
-rw-r--r--app/serializers/analytics_issue_serializer.rb3
-rw-r--r--app/serializers/analytics_merge_request_entity.rb7
-rw-r--r--app/serializers/analytics_merge_request_serializer.rb3
-rw-r--r--app/serializers/build_entity.rb16
-rw-r--r--app/serializers/commit_entity.rb7
-rw-r--r--app/serializers/deployment_entity.rb4
-rw-r--r--app/serializers/entity_date_helper.rb35
-rw-r--r--app/serializers/environment_entity.rb11
-rw-r--r--app/serializers/issuable_entity.rb16
-rw-r--r--app/serializers/issue_entity.rb9
-rw-r--r--app/serializers/issue_serializer.rb3
-rw-r--r--app/serializers/label_entity.rb11
-rw-r--r--app/serializers/merge_request_entity.rb14
-rw-r--r--app/serializers/merge_request_serializer.rb3
-rw-r--r--app/services/after_branch_delete_service.rb23
-rw-r--r--app/services/auth/container_registry_authentication_service.rb18
-rw-r--r--app/services/chat_names/authorize_user_service.rb38
-rw-r--r--app/services/chat_names/find_user_service.rb26
-rw-r--r--app/services/ci/send_pipeline_notification_service.rb19
-rw-r--r--app/services/ci/stop_environments_service.rb29
-rw-r--r--app/services/delete_merged_branches_service.rb18
-rw-r--r--app/services/destroy_group_service.rb10
-rw-r--r--app/services/git_push_service.rb56
-rw-r--r--app/services/issuable_base_service.rb4
-rw-r--r--app/services/issues/close_service.rb13
-rw-r--r--app/services/merge_requests/add_todo_when_build_fails_service.rb7
-rw-r--r--app/services/merge_requests/build_service.rb4
-rw-r--r--app/services/merge_requests/refresh_service.rb21
-rw-r--r--app/services/notes/create_service.rb9
-rw-r--r--app/services/notification_service.rb55
-rw-r--r--app/services/projects/create_service.rb11
-rw-r--r--app/services/projects/participants_service.rb9
-rw-r--r--app/services/slash_commands/interpret_service.rb4
-rw-r--r--app/services/user_project_access_changed_service.rb9
-rw-r--r--app/views/admin/application_settings/_form.html.haml25
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/groups/edit.html.haml2
-rw-r--r--app/views/admin/groups/new.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml22
-rw-r--r--app/views/devise/sessions/_new_base.html.haml4
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml3
-rw-r--r--app/views/discussions/_discussion.html.haml7
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/groups/milestones/new.html.haml2
-rw-r--r--app/views/groups/show.html.haml26
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml1
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml2
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml14
-rw-r--r--app/views/notify/pipeline_success_email.html.haml12
-rw-r--r--app/views/notify/repository_push_email.html.haml11
-rw-r--r--app/views/profiles/accounts/show.html.haml32
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml27
-rw-r--r--app/views/profiles/chat_names/index.html.haml30
-rw-r--r--app/views/profiles/chat_names/new.html.haml15
-rw-r--r--app/views/projects/_last_commit.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml28
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml112
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml44
-rw-r--r--app/views/projects/boards/components/_card.html.haml62
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-rw-r--r--app/views/projects/boards/index.html.haml19
-rw-r--r--app/views/projects/boards/show.html.haml19
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml5
-rw-r--r--app/views/projects/builds/_table.html.haml3
-rw-r--r--app/views/projects/builds/show.html.haml52
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml11
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml2
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml10
-rw-r--r--app/views/projects/commit/_commit_box.html.haml13
-rw-r--r--app/views/projects/commit/_pipeline.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml9
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/diffs/_line.html.haml4
-rw-r--r--app/views/projects/diffs/_stats.html.haml11
-rw-r--r--app/views/projects/environments/_environment.html.haml35
-rw-r--r--app/views/projects/environments/_header_title.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml58
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml10
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_issue_by_email.html.haml21
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml16
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml3
-rw-r--r--app/views/projects/merge_requests/_show.html.haml17
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml5
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml5
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml4
-rw-r--r--app/views/projects/network/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml11
-rw-r--r--app/views/projects/pipelines/_info.html.haml68
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml51
-rw-r--r--app/views/projects/pipelines/show.html.haml8
-rw-r--r--app/views/projects/refs/logs_tree.js.haml5
-rw-r--r--app/views/projects/services/index.html.haml5
-rw-r--r--app/views/projects/show.html.haml7
-rw-r--r--app/views/projects/triggers/index.html.haml20
-rw-r--r--app/views/search/results/_blob.html.haml12
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/shared/_label.html.haml55
-rw-r--r--app/views/shared/_service_settings.html.haml41
-rw-r--r--app/views/shared/icons/_icon_status_skipped.svg2
-rw-r--r--app/views/shared/issuable/_form.html.haml72
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml32
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/workers/authorized_projects_worker.rb15
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/delete_merged_branches_worker.rb20
-rw-r--r--app/workers/new_note_worker.rb12
-rw-r--r--app/workers/pipeline_metrics_worker.rb4
-rw-r--r--app/workers/pipeline_notification_worker.rb12
-rw-r--r--app/workers/process_commit_worker.rb67
-rw-r--r--changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml4
-rw-r--r--changelogs/unreleased/21076-deleted-merged-branches.yml4
-rw-r--r--changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml4
-rw-r--r--changelogs/unreleased/21992-disable-access-requests-by-default.yml4
-rw-r--r--changelogs/unreleased/22307-pipeline-link-in-builds-view.yml4
-rw-r--r--changelogs/unreleased/22539-display-folders.yml4
-rw-r--r--changelogs/unreleased/22699-group-permssion-background-migration.yml4
-rw-r--r--changelogs/unreleased/22790-mention-autocomplete-avatar.yml4
-rw-r--r--changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml4
-rw-r--r--changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml4
-rw-r--r--changelogs/unreleased/23223-group-deletion-race-condition.yml4
-rw-r--r--changelogs/unreleased/23584-triggering-builds-from-webhooks.yml4
-rw-r--r--changelogs/unreleased/23637-title-bar-pipelines.yml4
-rw-r--r--changelogs/unreleased/23731-add-param-to-user-api.yml4
-rw-r--r--changelogs/unreleased/23990-project-show-error-when-empty-repo.yml4
-rw-r--r--changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml4
-rw-r--r--changelogs/unreleased/24010-double-event-trigger.yml4
-rw-r--r--changelogs/unreleased/24048-dropdown-issue-with-devider.yml4
-rw-r--r--changelogs/unreleased/24070-project-margins.yml4
-rw-r--r--changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml4
-rw-r--r--changelogs/unreleased/24107-slack-comment-link.yml4
-rw-r--r--changelogs/unreleased/24255-search-fix.yml4
-rw-r--r--changelogs/unreleased/24276-usernames-with-dots.yml4
-rw-r--r--changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml4
-rw-r--r--changelogs/unreleased/24369-remove-additional-padding.yml4
-rw-r--r--changelogs/unreleased/24492-promise-polyfill.yml4
-rw-r--r--changelogs/unreleased/24496-fix-internal-api-project-lookup.yml4
-rw-r--r--changelogs/unreleased/24627-fix-bad-mr-error-message.yml4
-rw-r--r--changelogs/unreleased/adam-build-missing-services-when-necessary.yml4
-rw-r--r--changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml4
-rw-r--r--changelogs/unreleased/add-api-label-id.yml4
-rw-r--r--changelogs/unreleased/add-chat-names.yml4
-rw-r--r--changelogs/unreleased/always-show-download-button.yml4
-rw-r--r--changelogs/unreleased/api-label-priorities.yml2
-rw-r--r--changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml4
-rw-r--r--changelogs/unreleased/assignee-dropdown-autocomplete.yml4
-rw-r--r--changelogs/unreleased/bugfix-html-only-mail.yml4
-rw-r--r--changelogs/unreleased/changelog-update.yml4
-rw-r--r--changelogs/unreleased/create-pipeline-endpoint.yml4
-rw-r--r--changelogs/unreleased/dz-fix-group-name-dot.yml4
-rw-r--r--changelogs/unreleased/faster_project_search.yml4
-rw-r--r--changelogs/unreleased/feature-api_owned_resource.yml4
-rw-r--r--changelogs/unreleased/feature-cycle-analytics-events.yml4
-rw-r--r--changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml4
-rw-r--r--changelogs/unreleased/feature-precalculate-authorized-projects.yml4
-rw-r--r--changelogs/unreleased/feature-subscribe-to-group-level-labels.yml4
-rw-r--r--changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml4
-rw-r--r--changelogs/unreleased/fix-Build-timeFor.yml4
-rw-r--r--changelogs/unreleased/fix-admin-ci-table.yml4
-rw-r--r--changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml4
-rw-r--r--changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml4
-rw-r--r--changelogs/unreleased/fix-help-page-links.yml4
-rw-r--r--changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml4
-rw-r--r--changelogs/unreleased/fix-require-build-script-configuration-entry.yml4
-rw-r--r--changelogs/unreleased/fix-search-input-padding.yml4
-rw-r--r--changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml4
-rw-r--r--changelogs/unreleased/fix-singin-redirect-for-fork-new.yml5
-rw-r--r--changelogs/unreleased/fix-slack-pipeline-event.yml4
-rw-r--r--changelogs/unreleased/fix-trace-patch-updated-at.yml4
-rw-r--r--changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml4
-rw-r--r--changelogs/unreleased/fix_navigation_bar_issuables_counters.yml4
-rw-r--r--changelogs/unreleased/forking-in-progress-title.yml4
-rw-r--r--changelogs/unreleased/issue-13823.yml4
-rw-r--r--changelogs/unreleased/issue-24512.yml4
-rw-r--r--changelogs/unreleased/issue-boards-counter-border-fix.yml4
-rw-r--r--changelogs/unreleased/issue_13232.yml4
-rw-r--r--changelogs/unreleased/issue_5541.yml4
-rw-r--r--changelogs/unreleased/jira_service_simplify.yml4
-rw-r--r--changelogs/unreleased/ldap_check_bind.yml4
-rw-r--r--changelogs/unreleased/less-intrusive-system-note.yml4
-rw-r--r--changelogs/unreleased/mailroom_idle_timeout.yml4
-rw-r--r--changelogs/unreleased/master-recursiveTree.yml4
-rw-r--r--changelogs/unreleased/milestone-project-require.yml4
-rw-r--r--changelogs/unreleased/namespace-validation.yml4
-rw-r--r--changelogs/unreleased/new-note-worker-record-not-found-fix.yml4
-rw-r--r--changelogs/unreleased/optimize-mr-index.yml4
-rw-r--r--changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml4
-rw-r--r--changelogs/unreleased/pipeline-notifications.yml6
-rw-r--r--changelogs/unreleased/process-commits-using-sidekiq.yml4
-rw-r--r--changelogs/unreleased/rack_attack_logging.yml4
-rw-r--r--changelogs/unreleased/related-mr-labels.yml4
-rw-r--r--changelogs/unreleased/remove-heading-space-from-diff-content.yml4
-rw-r--r--changelogs/unreleased/remove-unnecessary-self-from-user-model.yml4
-rw-r--r--changelogs/unreleased/repository-name-emojis4
-rw-r--r--changelogs/unreleased/setter-for-key.yml4
-rw-r--r--changelogs/unreleased/sh-bump-omniauth-gitlab.yml4
-rw-r--r--changelogs/unreleased/sidekiq-job-throttling.yml4
-rw-r--r--changelogs/unreleased/sidekiq_default_retries.yml4
-rw-r--r--changelogs/unreleased/sort-api-groups.yml4
-rw-r--r--changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml4
-rw-r--r--changelogs/unreleased/upgrade-timeago.yml4
-rw-r--r--changelogs/unreleased/use-separate-token-for-incoming-email.yml4
-rw-r--r--changelogs/unreleased/user-dropdown-multiple-requests-fix.yml4
-rw-r--r--changelogs/unreleased/user_filter_auth.yml4
-rw-r--r--changelogs/unreleased/zj-slash-commands-mattermost.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/database.yml.mysql12
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/devise.rb23
-rw-r--r--config/initializers/rack_attack_logging.rb7
-rw-r--r--config/initializers/routing_draw.rb7
-rw-r--r--config/initializers/sidekiq.rb5
-rw-r--r--config/mail_room.yml2
-rw-r--r--config/no_todos_messages.yml11
-rw-r--r--config/routes.rb6
-rw-r--r--config/routes/git_http.rb37
-rw-r--r--config/routes/group.rb46
-rw-r--r--config/routes/profile.rb7
-rw-r--r--config/routes/project.rb197
-rw-r--r--config/routes/repository.rb110
-rw-r--r--config/routes/user.rb45
-rw-r--r--config/routes/wiki.rb16
-rw-r--r--config/sidekiq_queues.yml3
-rw-r--r--config/unicorn.rb.example4
-rw-r--r--db/fixtures/development/04_project.rb1
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb3
-rw-r--r--db/fixtures/support/serialized_transaction.rb9
-rw-r--r--db/fixtures/test/001_repo.rb0
-rw-r--r--db/migrate/20160819232256_add_incoming_email_token_to_users.rb16
-rw-r--r--db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb5
-rw-r--r--db/migrate/20161010142410_create_project_authorizations.rb15
-rw-r--r--db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb9
-rw-r--r--db/migrate/20161020075734_default_request_access_groups.rb12
-rw-r--r--db/migrate/20161020075830_default_request_access_projects.rb12
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb33
-rw-r--r--db/migrate/20161031171301_add_project_id_to_subscriptions.rb14
-rw-r--r--db/migrate/20161031174110_migrate_subscriptions_project_id.rb44
-rw-r--r--db/migrate/20161031181638_add_unique_index_to_subscriptions.rb18
-rw-r--r--db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb31
-rw-r--r--db/migrate/20161113184239_create_user_chat_names_table.rb21
-rw-r--r--db/migrate/20161117114805_remove_undeleted_groups.rb16
-rw-r--r--db/migrate/20161118183841_add_commit_events_to_services.rb15
-rw-r--r--db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb (renamed from db/migrate/20161011222551_remove_inactive_jira_service_properties.rb)0
-rw-r--r--db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb49
-rw-r--r--db/schema.rb47
-rw-r--r--doc/README.md1
-rw-r--r--doc/administration/auth/ldap.md22
-rw-r--r--doc/administration/high_availability/README.md33
-rw-r--r--doc/administration/high_availability/nfs.md2
-rw-r--r--doc/administration/high_availability/redis.md902
-rw-r--r--doc/administration/high_availability/redis_source.md366
-rw-r--r--doc/administration/img/raketasks/check_repos_output.png (renamed from doc/raketasks/check_repos_output.png)bin35333 -> 35333 bytes
-rw-r--r--doc/administration/operations.md1
-rw-r--r--doc/administration/operations/img/sidekiq_job_throttling.pngbin0 -> 114784 bytes
-rw-r--r--doc/administration/operations/sidekiq_job_throttling.md33
-rw-r--r--doc/administration/raketasks/check.md97
-rw-r--r--doc/administration/reply_by_email.md10
-rw-r--r--doc/api/branches.md18
-rw-r--r--doc/api/builds.md10
-rw-r--r--doc/api/groups.md18
-rw-r--r--doc/api/labels.md189
-rw-r--r--doc/api/notification_settings.md12
-rw-r--r--doc/api/pipelines.md46
-rw-r--r--doc/api/projects.md4
-rw-r--r--doc/api/repositories.md54
-rw-r--r--doc/api/users.md58
-rw-r--r--doc/ci/README.md10
-rw-r--r--doc/ci/docker/using_docker_build.md69
-rw-r--r--doc/ci/environments.md526
-rw-r--r--doc/ci/examples/test-scala-application.md41
-rw-r--r--doc/ci/img/deployments_view.pngbin0 -> 57598 bytes
-rw-r--r--doc/ci/img/environments_available_staging.pngbin0 -> 27398 bytes
-rw-r--r--doc/ci/img/environments_dynamic_groups.pngbin0 -> 134164 bytes
-rw-r--r--doc/ci/img/environments_link_url.pngbin0 -> 33561 bytes
-rw-r--r--doc/ci/img/environments_link_url_deployments.pngbin0 -> 19652 bytes
-rw-r--r--doc/ci/img/environments_link_url_mr.pngbin0 -> 47347 bytes
-rw-r--r--doc/ci/img/environments_manual_action_builds.pngbin0 -> 27170 bytes
-rw-r--r--doc/ci/img/environments_manual_action_deployments.pngbin0 -> 34504 bytes
-rw-r--r--doc/ci/img/environments_manual_action_environments.pngbin0 -> 40297 bytes
-rw-r--r--doc/ci/img/environments_manual_action_pipelines.pngbin0 -> 42212 bytes
-rw-r--r--doc/ci/img/environments_manual_action_single_pipeline.pngbin0 -> 42233 bytes
-rw-r--r--doc/ci/img/environments_mr_review_app.pngbin0 -> 39780 bytes
-rw-r--r--doc/ci/img/environments_view.pngbin0 -> 57534 bytes
-rw-r--r--doc/ci/review_apps/img/review_apps_preview_in_mr.pngbin0 -> 28689 bytes
-rw-r--r--doc/ci/review_apps/index.md124
-rw-r--r--doc/ci/triggers/README.md24
-rw-r--r--doc/ci/yaml/README.md59
-rw-r--r--doc/development/README.md3
-rw-r--r--doc/development/frontend.md51
-rw-r--r--doc/development/gotchas.md89
-rw-r--r--doc/development/instrumentation.md10
-rw-r--r--doc/development/limit_ee_conflicts.md272
-rw-r--r--doc/development/migration_style_guide.md12
-rw-r--r--doc/development/shell_commands.md8
-rw-r--r--doc/development/testing.md2
-rw-r--r--doc/development/ux_guide/basics.md94
-rw-r--r--doc/development/ux_guide/components.md254
-rw-r--r--doc/development/ux_guide/copy.md99
-rw-r--r--doc/development/ux_guide/features.md57
-rw-r--r--doc/development/ux_guide/img/button-primary.pngbin0 -> 8410 bytes
-rw-r--r--doc/development/ux_guide/img/button-secondary.pngbin0 -> 11160 bytes
-rw-r--r--doc/development/ux_guide/img/color-blue.pngbin0 -> 3865 bytes
-rw-r--r--doc/development/ux_guide/img/color-green.pngbin0 -> 4127 bytes
-rw-r--r--doc/development/ux_guide/img/color-grey.pngbin0 -> 2384 bytes
-rw-r--r--doc/development/ux_guide/img/color-orange.pngbin0 -> 4698 bytes
-rw-r--r--doc/development/ux_guide/img/color-red.pngbin0 -> 3669 bytes
-rw-r--r--doc/development/ux_guide/img/components-alerts.pngbin0 -> 46785 bytes
-rw-r--r--doc/development/ux_guide/img/components-anchorlinks.pngbin0 -> 36456 bytes
-rw-r--r--doc/development/ux_guide/img/components-contentblock.pngbin0 -> 19841 bytes
-rw-r--r--doc/development/ux_guide/img/components-counts.pngbin0 -> 2438 bytes
-rw-r--r--doc/development/ux_guide/img/components-coverblock.pngbin0 -> 15757 bytes
-rw-r--r--doc/development/ux_guide/img/components-dateexact.pngbin0 -> 5609 bytes
-rw-r--r--doc/development/ux_guide/img/components-daterelative.pngbin0 -> 5843 bytes
-rw-r--r--doc/development/ux_guide/img/components-dropdown.pngbin0 -> 60448 bytes
-rw-r--r--doc/development/ux_guide/img/components-fileholder.pngbin0 -> 4953 bytes
-rw-r--r--doc/development/ux_guide/img/components-horizontalform.pngbin0 -> 5708 bytes
-rw-r--r--doc/development/ux_guide/img/components-listinsidepanel.pngbin0 -> 3962 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithavatar.pngbin0 -> 7952 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithhover.pngbin0 -> 3313 bytes
-rw-r--r--doc/development/ux_guide/img/components-panels.pngbin0 -> 32886 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencehover.pngbin0 -> 11519 bytes
-rw-r--r--doc/development/ux_guide/img/components-referenceissues.pngbin0 -> 14587 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencelabels.pngbin0 -> 4643 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemilestone.pngbin0 -> 2468 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemrs.pngbin0 -> 12646 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencepeople.pngbin0 -> 7214 bytes
-rw-r--r--doc/development/ux_guide/img/components-rowcontentblock.pngbin0 -> 19730 bytes
-rw-r--r--doc/development/ux_guide/img/components-simplelist.pngbin0 -> 3078 bytes
-rw-r--r--doc/development/ux_guide/img/components-table.pngbin0 -> 7668 bytes
-rw-r--r--doc/development/ux_guide/img/components-verticalform.pngbin0 -> 6541 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissuebutton.pngbin0 -> 26541 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-addissueform.pngbin0 -> 38242 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissuebutton.pngbin0 -> 16466 bytes
-rw-r--r--doc/development/ux_guide/img/copy-form-editissueform.pngbin0 -> 40660 bytes
-rw-r--r--doc/development/ux_guide/img/features-contextualnav.pngbin0 -> 8051 bytes
-rw-r--r--doc/development/ux_guide/img/features-emptystates.pngbin0 -> 114540 bytes
-rw-r--r--doc/development/ux_guide/img/features-filters.pngbin0 -> 4529 bytes
-rw-r--r--doc/development/ux_guide/img/features-globalnav.pngbin0 -> 8953 bytes
-rw-r--r--doc/development/ux_guide/img/icon-add.pngbin0 -> 155 bytes
-rw-r--r--doc/development/ux_guide/img/icon-close.pngbin0 -> 225 bytes
-rw-r--r--doc/development/ux_guide/img/icon-edit.pngbin0 -> 231 bytes
-rw-r--r--doc/development/ux_guide/img/icon-notification.pngbin0 -> 259 bytes
-rw-r--r--doc/development/ux_guide/img/icon-rss.pngbin0 -> 307 bytes
-rw-r--r--doc/development/ux_guide/img/icon-subscribe.pngbin0 -> 348 bytes
-rw-r--r--doc/development/ux_guide/img/icon-trash.pngbin0 -> 380 bytes
-rw-r--r--doc/development/ux_guide/img/monospacefont-sample.pngbin0 -> 14282 bytes
-rw-r--r--doc/development/ux_guide/img/sourcesanspro-sample.pngbin0 -> 10948 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-contentitemtitle.pngbin0 -> 7463 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-header.pngbin0 -> 6103 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-systeminformationblock.pngbin0 -> 15412 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-ux.pngbin0 -> 7673 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-placement.pngbin0 -> 2645 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-usage.pngbin0 -> 9160 bytes
-rw-r--r--doc/development/ux_guide/index.md58
-rw-r--r--doc/development/ux_guide/principles.md17
-rw-r--r--doc/development/ux_guide/resources.md13
-rw-r--r--doc/development/ux_guide/surfaces.md47
-rw-r--r--doc/development/ux_guide/tips.md44
-rw-r--r--doc/development/ux_guide/users.md16
-rw-r--r--doc/gitlab-basics/create-project.md4
-rw-r--r--doc/install/requirements.md9
-rw-r--r--doc/integration/README.md10
-rw-r--r--doc/integration/jira.md196
-rw-r--r--doc/integration/shibboleth.md2
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin30956 -> 33943 bytes
-rw-r--r--doc/project_services/img/jira_add_gitlab_commit_message.pngbin0 -> 46590 bytes
-rw-r--r--doc/project_services/img/jira_add_user_to_group.pngbin0 -> 41994 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group.pngbin0 -> 32934 bytes
-rw-r--r--doc/project_services/img/jira_create_new_group_name.pngbin0 -> 9054 bytes
-rw-r--r--doc/project_services/img/jira_create_new_user.pngbin0 -> 21081 bytes
-rw-r--r--doc/project_services/img/jira_group_access.pngbin0 -> 32210 bytes
-rw-r--r--doc/project_services/img/jira_issue_closed.pngbin0 -> 77028 bytes
-rw-r--r--doc/project_services/img/jira_issue_reference.pngbin0 -> 36188 bytes
-rw-r--r--doc/project_services/img/jira_issues_workflow.pngbin0 -> 87067 bytes
-rw-r--r--doc/project_services/img/jira_merge_request_close.pngbin0 -> 102835 bytes
-rw-r--r--doc/project_services/img/jira_project_name.pngbin0 -> 41572 bytes
-rw-r--r--doc/project_services/img/jira_reference_commit_message_in_jira_issue.pngbin0 -> 33706 bytes
-rw-r--r--doc/project_services/img/jira_service.pngbin0 -> 56834 bytes
-rw-r--r--doc/project_services/img/jira_service_close_issue.pngbin0 -> 79569 bytes
-rw-r--r--doc/project_services/img/jira_service_page.pngbin0 -> 36280 bytes
-rw-r--r--doc/project_services/img/jira_submit_gitlab_merge_request.pngbin0 -> 51913 bytes
-rw-r--r--doc/project_services/img/jira_user_management_link.pngbin0 -> 43095 bytes
-rw-r--r--doc/project_services/img/jira_workflow_screenshot.pngbin0 -> 111093 bytes
-rw-r--r--doc/project_services/jira.md247
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/raketasks/check.md62
-rw-r--r--doc/university/README.md3
-rwxr-xr-xdoc/university/training/gitlab_flow.md53
-rw-r--r--doc/university/training/gitlab_flow/feature_branches.pngbin0 -> 20600 bytes
-rw-r--r--doc/university/training/gitlab_flow/production_branch.pngbin0 -> 21716 bytes
-rw-r--r--doc/university/training/gitlab_flow/release_branches.pngbin0 -> 44173 bytes
-rwxr-xr-xdoc/university/training/index.md6
-rw-r--r--doc/university/training/logo.pngbin0 -> 33117 bytes
-rwxr-xr-xdoc/university/training/topics/additional_resources.md8
-rwxr-xr-xdoc/university/training/topics/agile_git.md33
-rwxr-xr-xdoc/university/training/topics/bisect.md81
-rwxr-xr-xdoc/university/training/topics/cherry_picking.md39
-rwxr-xr-xdoc/university/training/topics/env_setup.md60
-rwxr-xr-xdoc/university/training/topics/explore_gitlab.md10
-rwxr-xr-xdoc/university/training/topics/feature_branching.md32
-rwxr-xr-xdoc/university/training/topics/getting_started.md95
-rwxr-xr-xdoc/university/training/topics/git_add.md33
-rwxr-xr-xdoc/university/training/topics/git_intro.md24
-rwxr-xr-xdoc/university/training/topics/git_log.md57
-rwxr-xr-xdoc/university/training/topics/gitlab_flow.md53
-rwxr-xr-xdoc/university/training/topics/merge_conflicts.md70
-rwxr-xr-xdoc/university/training/topics/merge_requests.md43
-rwxr-xr-xdoc/university/training/topics/rollback_commits.md81
-rwxr-xr-xdoc/university/training/topics/stash.md86
-rwxr-xr-xdoc/university/training/topics/subtree.md55
-rwxr-xr-xdoc/university/training/topics/tags.md38
-rwxr-xr-xdoc/university/training/topics/unstage.md31
-rwxr-xr-xdoc/university/training/user_training.md392
-rw-r--r--doc/user/markdown.md158
-rw-r--r--doc/user/permissions.md4
-rw-r--r--doc/workflow/gitlab_flow.md2
-rw-r--r--doc/workflow/notifications.md3
-rw-r--r--doc/workflow/todos.md6
-rw-r--r--features/profile/profile.feature5
-rw-r--r--features/project/network_graph.feature2
-rw-r--r--features/project/source/git_blame.feature10
-rw-r--r--features/snippet_search.feature20
-rw-r--r--features/snippets/discover.feature13
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/profile/profile.rb12
-rw-r--r--features/steps/project/merge_requests.rb18
-rw-r--r--features/steps/project/network_graph.rb4
-rw-r--r--features/steps/project/source/git_blame.rb19
-rw-r--r--features/steps/shared/search.rb11
-rw-r--r--features/steps/snippet_search.rb55
-rw-r--r--features/steps/snippets/discover.rb21
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/branches.rb12
-rw-r--r--lib/api/broadcast_messages.rb6
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/entities.rb23
-rw-r--r--lib/api/environments.rb7
-rw-r--r--lib/api/groups.rb156
-rw-r--r--lib/api/helpers.rb24
-rw-r--r--lib/api/helpers/internal_helpers.rb57
-rw-r--r--lib/api/internal.rb38
-rw-r--r--lib/api/issues.rb10
-rw-r--r--lib/api/labels.rb11
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_requests.rb282
-rw-r--r--lib/api/milestones.rb112
-rw-r--r--lib/api/notes.rb124
-rw-r--r--lib/api/notification_settings.rb7
-rw-r--r--lib/api/pipelines.rb21
-rw-r--r--lib/api/project_hooks.rb10
-rw-r--r--lib/api/repositories.rb95
-rw-r--r--lib/api/runners.rb115
-rw-r--r--lib/api/services.rb29
-rw-r--r--lib/api/session.rb19
-rw-r--r--lib/api/subscriptions.rb49
-rw-r--r--lib/api/system_hooks.rb4
-rw-r--r--lib/api/tags.rb3
-rw-r--r--lib/api/triggers.rb78
-rw-r--r--lib/api/users.rb503
-rw-r--r--lib/banzai/filter/autolink_filter.rb38
-rw-r--r--lib/banzai/reference_parser/base_parser.rb16
-rw-r--r--lib/banzai/reference_parser/commit_parser.rb6
-rw-r--r--lib/banzai/reference_parser/commit_range_parser.rb6
-rw-r--r--lib/banzai/reference_parser/external_issue_parser.rb6
-rw-r--r--lib/banzai/reference_parser/label_parser.rb6
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb6
-rw-r--r--lib/banzai/reference_parser/milestone_parser.rb6
-rw-r--r--lib/banzai/reference_parser/snippet_parser.rb6
-rw-r--r--lib/banzai/reference_parser/user_parser.rb34
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/constraints/constrainer_helper.rb15
-rw-r--r--lib/constraints/group_url_constrainer.rb16
-rw-r--r--lib/constraints/namespace_url_constrainer.rb24
-rw-r--r--lib/constraints/user_url_constrainer.rb16
-rw-r--r--lib/extracts_path.rb16
-rw-r--r--lib/gitlab/chat_commands/base_command.rb49
-rw-r--r--lib/gitlab/chat_commands/command.rb61
-rw-r--r--lib/gitlab/chat_commands/issue_command.rb17
-rw-r--r--lib/gitlab/chat_commands/issue_create.rb24
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb17
-rw-r--r--lib/gitlab/chat_name_token.rb45
-rw-r--r--lib/gitlab/ci/config.rb43
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb (renamed from lib/gitlab/ci/config/node/artifacts.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb (renamed from lib/gitlab/ci/config/node/attributable.rb)2
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb (renamed from lib/gitlab/ci/config/node/boolean.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb (renamed from lib/gitlab/ci/config/node/cache.rb)10
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb (renamed from lib/gitlab/ci/config/node/commands.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb (renamed from lib/gitlab/ci/config/node/configurable.rb)8
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb (renamed from lib/gitlab/ci/config/node/environment.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb (renamed from lib/gitlab/ci/config/node/factory.rb)30
-rw-r--r--lib/gitlab/ci/config/entry/global.rb (renamed from lib/gitlab/ci/config/node/global.rb)24
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb (renamed from lib/gitlab/ci/config/node/hidden.rb)6
-rw-r--r--lib/gitlab/ci/config/entry/image.rb (renamed from lib/gitlab/ci/config/node/image.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb (renamed from lib/gitlab/ci/config/node/job.rb)62
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb (renamed from lib/gitlab/ci/config/node/jobs.rb)8
-rw-r--r--lib/gitlab/ci/config/entry/key.rb (renamed from lib/gitlab/ci/config/node/key.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb (renamed from lib/gitlab/ci/config/node/legacy_validation_helpers.rb)2
-rw-r--r--lib/gitlab/ci/config/entry/node.rb (renamed from lib/gitlab/ci/config/node/entry.rb)6
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb (renamed from lib/gitlab/ci/config/node/paths.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/script.rb (renamed from lib/gitlab/ci/config/node/script.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/services.rb (renamed from lib/gitlab/ci/config/node/services.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb (renamed from lib/gitlab/ci/config/node/stage.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb (renamed from lib/gitlab/ci/config/node/stages.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb (renamed from lib/gitlab/ci/config/node/trigger.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb (renamed from lib/gitlab/ci/config/node/undefined.rb)8
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb (renamed from lib/gitlab/ci/config/node/unspecified.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb (renamed from lib/gitlab/ci/config/node/validatable.rb)4
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb (renamed from lib/gitlab/ci/config/node/validator.rb)12
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb (renamed from lib/gitlab/ci/config/node/validators.rb)2
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb (renamed from lib/gitlab/ci/config/node/variables.rb)4
-rw-r--r--lib/gitlab/contributions_calendar.rb74
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/cycle_analytics/base_event.rb57
-rw-r--r--lib/gitlab/cycle_analytics/code_event.rb28
-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_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/issue_event.rb27
-rw-r--r--lib/gitlab/cycle_analytics/merge_request_allowed.rb9
-rw-r--r--lib/gitlab/cycle_analytics/metrics_fetcher.rb60
-rw-r--r--lib/gitlab/cycle_analytics/metrics_tables.rb37
-rw-r--r--lib/gitlab/cycle_analytics/plan_event.rb44
-rw-r--r--lib/gitlab/cycle_analytics/production_event.rb26
-rw-r--r--lib/gitlab/cycle_analytics/review_event.rb25
-rw-r--r--lib/gitlab/cycle_analytics/staging_event.rb31
-rw-r--r--lib/gitlab/cycle_analytics/test_event.rb13
-rw-r--r--lib/gitlab/cycle_analytics/updater.rb30
-rw-r--r--lib/gitlab/database.rb7
-rw-r--r--lib/gitlab/database/date_time.rb28
-rw-r--r--lib/gitlab/diff/file.rb15
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb2
-rw-r--r--lib/gitlab/ee_compat_check.rb152
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb8
-rw-r--r--lib/gitlab/email/html_parser.rb34
-rw-r--r--lib/gitlab/email/reply_parser.rb19
-rw-r--r--lib/gitlab/git_access.rb91
-rw-r--r--lib/gitlab/github_import/importer.rb50
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb4
-rw-r--r--lib/gitlab/incoming_email.rb12
-rw-r--r--lib/gitlab/ldap/adapter.rb4
-rw-r--r--lib/gitlab/ldap/authentication.rb6
-rw-r--r--lib/gitlab/ldap/config.rb69
-rw-r--r--lib/gitlab/mail_room.rb1
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/project_search_results.rb75
-rw-r--r--lib/gitlab/regex.rb19
-rw-r--r--lib/gitlab/sidekiq_throttler.rb23
-rw-r--r--lib/mattermost/presenter.rb117
-rw-r--r--lib/tasks/gitlab/check.rake39
-rw-r--r--lib/tasks/gitlab/cleanup.rake23
-rw-r--r--lib/tasks/gitlab/dev.rake23
-rw-r--r--package.json1
-rwxr-xr-xscripts/prepare_build.sh9
-rw-r--r--spec/config/mail_room_spec.rb1
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb22
-rw-r--r--spec/controllers/help_controller_spec.rb38
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb58
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb33
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb58
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb30
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb25
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb47
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb18
-rw-r--r--spec/factories/chat_names.rb16
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/environments.rb28
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/projects.rb14
-rw-r--r--spec/factories/subscriptions.rb7
-rw-r--r--spec/features/admin/admin_groups_spec.rb35
-rw-r--r--spec/features/boards/boards_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb44
-rw-r--r--spec/features/environment_spec.rb161
-rw-r--r--spec/features/environments_spec.rb265
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb14
-rw-r--r--spec/features/global_search_spec.rb28
-rw-r--r--spec/features/groups/issues_spec.rb8
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb8
-rw-r--r--spec/features/groups_spec.rb8
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb35
-rw-r--r--spec/features/issues/new_branch_button_spec.rb8
-rw-r--r--spec/features/issues_spec.rb23
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb17
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb4
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb4
-rw-r--r--spec/features/profile_spec.rb29
-rw-r--r--spec/features/profiles/chat_names_spec.rb77
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb2
-rw-r--r--spec/features/projects/files/browse_files_spec.rb21
-rw-r--r--spec/features/projects/labels/subscription_spec.rb74
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb4
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb19
-rw-r--r--spec/features/projects/pipelines_spec.rb3
-rw-r--r--spec/features/projects/project_settings_spec.rb17
-rw-r--r--spec/features/search_spec.rb26
-rw-r--r--spec/features/snippets/explore_spec.rb16
-rw-r--r--spec/features/snippets/search_snippets_spec.rb66
-rw-r--r--spec/features/todos/todos_spec.rb6
-rw-r--r--spec/features/unsubscribe_links_spec.rb14
-rw-r--r--spec/features/users_spec.rb19
-rw-r--r--spec/finders/access_requests_finder_spec.rb15
-rw-r--r--spec/finders/group_projects_finder_spec.rb5
-rw-r--r--spec/finders/labels_finder_spec.rb15
-rw-r--r--spec/fixtures/emails/outlook_html.eml140
-rw-r--r--spec/fixtures/emails/wrong_incoming_email_token.eml (renamed from spec/fixtures/emails/wrong_authentication_token.eml)0
-rw-r--r--spec/helpers/application_helper_spec.rb28
-rw-r--r--spec/helpers/components_helper_spec.rb21
-rw-r--r--spec/helpers/diff_helper_spec.rb2
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb2
-rw-r--r--spec/helpers/members_helper_spec.rb8
-rw-r--r--spec/helpers/preferences_helper_spec.rb41
-rw-r--r--spec/helpers/search_helper_spec.rb32
-rw-r--r--spec/javascripts/.eslintrc11
-rw-r--r--spec/javascripts/application_spec.js2
-rw-r--r--spec/javascripts/awards_handler_spec.js2
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js2
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js2
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6215
-rw-r--r--spec/javascripts/boards/list_spec.js.es65
-rw-r--r--spec/javascripts/boards/mock_data.js.es64
-rw-r--r--spec/javascripts/build_spec.js.es6190
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es637
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es622
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es6215
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es648
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es628
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es669
-rw-r--r--spec/javascripts/environments/mock_data.js.es6135
-rw-r--r--spec/javascripts/extensions/array_spec.js2
-rw-r--r--spec/javascripts/extensions/jquery_spec.js2
-rw-r--r--spec/javascripts/fixtures/build.html.haml62
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js2
-rw-r--r--spec/javascripts/fixtures/environments/element.html.haml1
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml9
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml11
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js2
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js2
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js2
-rw-r--r--spec/javascripts/header_spec.js2
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/line_highlighter_spec.js2
-rw-r--r--spec/javascripts/merge_request_spec.js2
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js2
-rw-r--r--spec/javascripts/merge_request_widget_spec.js8
-rw-r--r--spec/javascripts/new_branch_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js2
-rw-r--r--spec/javascripts/project_title_spec.js2
-rw-r--r--spec/javascripts/right_sidebar_spec.js2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js2
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/spec_helper.js2
-rw-r--r--spec/javascripts/syntax_highlight_spec.js2
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js2
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js2
-rw-r--r--spec/javascripts/u2f/register_spec.js2
-rw-r--r--spec/javascripts/vue_common_components/commit_spec.js.es6126
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb22
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb42
-rw-r--r--spec/lib/banzai/reference_parser/base_parser_spec.rb35
-rw-r--r--spec/lib/banzai/reference_parser/commit_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/commit_range_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/external_issue_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb10
-rw-r--r--spec/lib/banzai/reference_parser/label_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/merge_request_parser_spec.rb13
-rw-r--r--spec/lib/banzai/reference_parser/milestone_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/snippet_parser_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/user_parser_spec.rb2
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb4
-rw-r--r--spec/lib/constraints/constrainer_helper_spec.rb20
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb17
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb35
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb12
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb55
-rw-r--r--spec/lib/gitlab/chat_commands/issue_create_spec.rb52
-rw-r--r--spec/lib/gitlab/chat_commands/issue_show_spec.rb40
-rw-r--r--spec/lib/gitlab/chat_name_token_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_spec.rb (renamed from spec/lib/gitlab/ci/config/node/artifacts_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/attributable_spec.rb (renamed from spec/lib/gitlab/ci/config/node/attributable_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/boolean_spec.rb (renamed from spec/lib/gitlab/ci/config/node/boolean_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb (renamed from spec/lib/gitlab/ci/config/node/cache_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/commands_spec.rb (renamed from spec/lib/gitlab/ci/config/node/commands_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/configurable_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb (renamed from spec/lib/gitlab/ci/config/node/environment_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/factory_spec.rb (renamed from spec/lib/gitlab/ci/config/node/factory_spec.rb)14
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb (renamed from spec/lib/gitlab/ci/config/node/global_spec.rb)68
-rw-r--r--spec/lib/gitlab/ci/config/entry/hidden_spec.rb (renamed from spec/lib/gitlab/ci/config/node/hidden_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb (renamed from spec/lib/gitlab/ci/config/node/image_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb (renamed from spec/lib/gitlab/ci/config/node/job_spec.rb)16
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb (renamed from spec/lib/gitlab/ci/config/node/jobs_spec.rb)6
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb (renamed from spec/lib/gitlab/ci/config/node/key_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/paths_spec.rb (renamed from spec/lib/gitlab/ci/config/node/paths_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/script_spec.rb (renamed from spec/lib/gitlab/ci/config/node/script_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb (renamed from spec/lib/gitlab/ci/config/node/services_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/stage_spec.rb (renamed from spec/lib/gitlab/ci/config/node/stage_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/stages_spec.rb (renamed from spec/lib/gitlab/ci/config/node/stages_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb (renamed from spec/lib/gitlab/ci/config/node/trigger_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/undefined_spec.rb (renamed from spec/lib/gitlab/ci/config/node/undefined_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/unspecified_spec.rb (renamed from spec/lib/gitlab/ci/config/node/unspecified_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/validatable_spec.rb (renamed from spec/lib/gitlab/ci/config/node/validatable_spec.rb)26
-rw-r--r--spec/lib/gitlab/ci/config/entry/validator_spec.rb (renamed from spec/lib/gitlab/ci/config/node/validator_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/entry/variables_spec.rb (renamed from spec/lib/gitlab/ci/config/node/variables_spec.rb)2
-rw-r--r--spec/lib/gitlab/ci/config/node/configurable_spec.rb67
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb104
-rw-r--r--spec/lib/gitlab/cycle_analytics/code_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb326
-rw-r--r--spec/lib/gitlab/cycle_analytics/issue_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/plan_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/production_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/review_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/shared_event_spec.rb21
-rw-r--r--spec/lib/gitlab/cycle_analytics/staging_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/test_event_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/updater_spec.rb25
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb24
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb8
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb25
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb3
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml3
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb120
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb2
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb23
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb51
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb3
-rw-r--r--spec/lib/gitlab/sidekiq_throttler_spec.rb28
-rw-r--r--spec/lib/light_url_builder_spec.rb119
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb15
-rw-r--r--spec/models/broadcast_message_spec.rb2
-rw-r--r--spec/models/build_spec.rb128
-rw-r--r--spec/models/chat_name_spec.rb16
-rw-r--r--spec/models/ci/build_spec.rb6
-rw-r--r--spec/models/ci/pipeline_spec.rb74
-rw-r--r--spec/models/concerns/access_requestable_spec.rb8
-rw-r--r--spec/models/concerns/issuable_spec.rb48
-rw-r--r--spec/models/concerns/subscribable_spec.rb117
-rw-r--r--spec/models/environment_spec.rb21
-rw-r--r--spec/models/event_spec.rb32
-rw-r--r--spec/models/external_issue_spec.rb8
-rw-r--r--spec/models/group_label_spec.rb2
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/guest_spec.rb47
-rw-r--r--spec/models/issue_collection_spec.rb67
-rw-r--r--spec/models/issue_spec.rb96
-rw-r--r--spec/models/key_spec.rb24
-rw-r--r--spec/models/member_spec.rb26
-rw-r--r--spec/models/project_group_link_spec.rb16
-rw-r--r--spec/models/project_services/chat_service_spec.rb15
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb6
-rw-r--r--spec/models/project_services/jira_service_spec.rb55
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb99
-rw-r--r--spec/models/project_services/pipeline_email_service_spec.rb15
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb16
-rw-r--r--spec/models/project_services/slack_service/pipeline_message_spec.rb4
-rw-r--r--spec/models/project_spec.rb90
-rw-r--r--spec/models/project_team_spec.rb8
-rw-r--r--spec/models/repository_spec.rb124
-rw-r--r--spec/models/service_spec.rb10
-rw-r--r--spec/models/subscription_spec.rb20
-rw-r--r--spec/models/user_spec.rb139
-rw-r--r--spec/policies/issue_policy_spec.rb119
-rw-r--r--spec/requests/api/access_requests_spec.rb22
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb32
-rw-r--r--spec/requests/api/branches_spec.rb16
-rw-r--r--spec/requests/api/groups_spec.rb57
-rw-r--r--spec/requests/api/internal_spec.rb40
-rw-r--r--spec/requests/api/issues_spec.rb4
-rw-r--r--spec/requests/api/labels_spec.rb57
-rw-r--r--spec/requests/api/members_spec.rb20
-rw-r--r--spec/requests/api/merge_requests_spec.rb6
-rw-r--r--spec/requests/api/milestones_spec.rb9
-rw-r--r--spec/requests/api/pipelines_spec.rb46
-rw-r--r--spec/requests/api/projects_spec.rb32
-rw-r--r--spec/requests/api/repositories_spec.rb35
-rw-r--r--spec/requests/api/runners_spec.rb4
-rw-r--r--spec/requests/api/services_spec.rb60
-rw-r--r--spec/requests/api/session_spec.rb16
-rw-r--r--spec/requests/api/system_hooks_spec.rb6
-rw-r--r--spec/requests/api/triggers_spec.rb9
-rw-r--r--spec/requests/api/users_spec.rb97
-rw-r--r--spec/requests/ci/api/builds_spec.rb86
-rw-r--r--spec/requests/git_http_spec.rb32
-rw-r--r--spec/requests/jwt_controller_spec.rb18
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb140
-rw-r--r--spec/routing/routing_spec.rb20
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb27
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb22
-rw-r--r--spec/serializers/analytics_generic_entity_spec.rb39
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb33
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb34
-rw-r--r--spec/serializers/build_entity_spec.rb10
-rw-r--r--spec/serializers/commit_entity_spec.rb6
-rw-r--r--spec/serializers/deployment_entity_spec.rb2
-rw-r--r--spec/serializers/entity_date_helper_spec.rb45
-rw-r--r--spec/serializers/environment_entity_spec.rb2
-rw-r--r--spec/serializers/environment_serializer_spec.rb2
-rw-r--r--spec/services/after_branch_delete_service_spec.rb15
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb25
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb36
-rw-r--r--spec/services/ci/send_pipeline_notification_service_spec.rb48
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb105
-rw-r--r--spec/services/delete_branch_service_spec.rb41
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb54
-rw-r--r--spec/services/destroy_group_service_spec.rb40
-rw-r--r--spec/services/git_push_service_spec.rb28
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb6
-rw-r--r--spec/services/issues/close_service_spec.rb49
-rw-r--r--spec/services/issues/update_service_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb4
-rw-r--r--spec/services/members/destroy_service_spec.rb1
-rw-r--r--spec/services/members/request_access_service_spec.rb31
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb33
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb6
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb22
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb10
-rw-r--r--spec/services/merge_requests/update_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb37
-rw-r--r--spec/services/notification_service_spec.rb184
-rw-r--r--spec/services/projects/create_service_spec.rb23
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb69
-rw-r--r--spec/spec_helper.rb6
-rw-r--r--spec/support/cycle_analytics_helpers.rb3
-rw-r--r--spec/support/database_connection_helpers.rb9
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/email_helpers.rb28
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb10
-rw-r--r--spec/support/jira_service_helper.rb8
-rw-r--r--spec/support/matchers/be_url.rb5
-rw-r--r--spec/support/notify_shared_examples.rb2
-rw-r--r--spec/support/project_features_apply_to_issuables_shared_examples.rb56
-rw-r--r--spec/support/reference_parser_shared_examples.rb43
-rw-r--r--spec/support/search_helpers.rb5
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb51
-rw-r--r--spec/views/projects/builds/_build.html.haml_spec.rb28
-rw-r--r--spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb28
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb133
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb22
-rw-r--r--spec/workers/build_email_worker_spec.rb2
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb19
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb4
-rw-r--r--spec/workers/new_note_worker_spec.rb49
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb18
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb131
-rw-r--r--spec/workers/process_commit_worker_spec.rb109
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js1159
-rw-r--r--vendor/assets/javascripts/jquery.timeago.js182
-rw-r--r--vendor/assets/javascripts/vue.full.js15894
-rw-r--r--vendor/assets/javascripts/vue.min.js10
1199 files changed, 29797 insertions, 15316 deletions
diff --git a/.gitattributes b/.gitattributes
index ab791a4cd6c..70cce05d2b5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1 @@
-CHANGELOG.md merge=union
*.js.es6 gitlab-language=javascript
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d04069df885..436e9ec6c60 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -61,20 +61,33 @@ update-knapsack:
- scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- rm -f knapsack/*_node_*.json
only:
- - master
-
-# Execute all testing suites
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
.use-db: &use-db
services:
- mysql:latest
- redis:alpine
+setup-test-env:
+ <<: *use-db
+ stage: prepare
+ script:
+ - bundle exec rake assets:precompile 2>/dev/null
+ - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
+ artifacts:
+ expire_in: 7d
+ paths:
+ - public/assets
+ - tmp/tests
+
+
.rspec-knapsack: &rspec-knapsack
stage: test
<<: *use-db
script:
- - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
@@ -92,7 +105,6 @@ update-knapsack:
stage: test
<<: *use-db
script:
- - bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
@@ -143,7 +155,10 @@ spinach 9 10: *spinach-knapsack
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
cache:
key: "ruby21"
paths:
@@ -213,11 +228,24 @@ rake downtime_check: *exec
rake ee_compat_check:
<<: *exec
only:
- - branches
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab/gitlabhq
except:
- master
- tags
+ - /^[\d-]+-stable(-ee)?$/
allow_failure: yes
+ cache:
+ key: "ruby231-ee_compat_check_repo"
+ paths:
+ - ee_compat_check/repo/
+ - vendor/ruby
+ artifacts:
+ name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+ when: on_failure
+ expire_in: 10d
+ paths:
+ - ee_compat_check/patches/*.patch
rake db:migrate:reset:
stage: test
@@ -273,7 +301,10 @@ bundler:audit:
stage: test
<<: *ruby-static-analysis
only:
- - master
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
@@ -284,6 +315,9 @@ migration paths:
SETUP_DB: "false"
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
script:
- git checkout HEAD .
- git fetch --tags
@@ -331,7 +365,7 @@ trigger_docs:
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds"
+ - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
only:
- master@gitlab-org/gitlab-ce
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ed09fb1db8..9f41cbc9228 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ entry.
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc)
- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
+- Change auto selection behaviour of emoji and slash commands to be more UX/Type friendly (Yann Gravrand)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
- Fix Markdown styling inside reference links (Jan Zdráhal)
- Create new issue board list after creating a new label
@@ -19,6 +20,7 @@ entry.
- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
+- Removed unneeded "Builds" and "Environments" link from project titles
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
- Refactor Jira service to use jira-ruby gem
@@ -71,14 +73,49 @@ entry.
- Fix applying GitHub-imported labels when importing job is interrupted
- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
- Updated commit SHA styling on the branches page.
+- Fix "Without projects" filter. !6611 (Ben Bodenmiller)
+- Fix 404 when visit /projects page
+
+## 8.13.6 (2016-11-17)
+
+- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002
+- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option. !7117
+- Fix relative links in Markdown wiki when displayed in "Project" tab. !7218
+- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry)
+- Fix cache for commit status in commits list to respect branches. !7372
+- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford)
+- Limit labels returned for a specific project as an administrator. !7496
+- Clicking "force remove source branch" label now toggles the checkbox again.
+- Allow commit note to be visible if repo is visible.
+- Fix project Visibility Level selector not using default values.
+
+## 8.13.5 (2016-11-08)
+
+- Restore unauthenticated access to public container registries
+- Fix showing pipeline status for a given commit from correct branch. !7034
+- Only skip group when it's actually a group in the "Share with group" select. !7262
+- Introduce round-robin project creation to spread load over multiple shards. !7266
+- Ensure merge request's "remove branch" accessors return booleans. !7267
+- Ensure external users are not able to clone disabled repositories.
+- Fix XSS issue in Markdown autolinker.
+- Respect event visibility in Gitlab::ContributionsCalendar.
+- Honour issue and merge request visibility in their respective finders.
+- Disable reference Markdown for unavailable features.
+- Fix lightweight tags not processed correctly by GitTagPushService. !6532
+- Allow owners to fetch source code in CI builds. !6943
+- Return conflict error in label API when title is taken by group label. !7014
+- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project. !7123
+- Fix builds tab visibility. !7178
+- Fix project features default values. !7181
+
+## 8.13.4
+
+- Pulled due to packaging error.
## 8.13.3 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
- Fixed Import/Export foreign key issue to do with project members.
-- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
-- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
-- Fix project features default values
- Changed build dropdown list length to be 6,5 builds long in the pipeline graph
## 8.13.2 (2016-10-31)
@@ -269,6 +306,10 @@ entry.
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
+## 8.12.9 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
## 8.12.8 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
@@ -533,6 +574,10 @@ entry.
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
+## 8.11.11 (2016-11-07)
+
+- Fix XSS issue in Markdown autolinker
+
## 8.11.10 (2016-11-02)
- Removes any symlinks before importing a project export file. CVE-2016-9086
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 67c30c2424c..659871a06a4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,8 +9,6 @@
- [Helping others](#helping-others)
- [I want to contribute!](#i-want-to-contribute)
- [Implement design & UI elements](#implement-design-ui-elements)
- - [Design reference](#design-reference)
- - [UI development kit](#ui-development-kit)
- [Issue tracker](#issue-tracker)
- [Feature proposals](#feature-proposals)
- [Issue tracker guidelines](#issue-tracker-guidelines)
@@ -90,7 +88,7 @@ This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Implement design & UI elements
-Please see the [UI Guide for building GitLab].
+Please see the [UX Guide for GitLab].
## Issue tracker
@@ -218,7 +216,10 @@ associated with in the description of the issue.
We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The features we would really like a merge request for are
listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
-and [EE][accepting-mrs-ee] but other improvements are also welcome.
+and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
+that if an issue is marked for the current milestone either before or while you
+are working on it, a team member may take over the merge request in order to
+ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create
a feedback issue (if there isn't one already) and leave a comment asking for it
@@ -469,5 +470,5 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
-[UI Guide for building GitLab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/ui_guide.md
+[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
diff --git a/Gemfile b/Gemfile
index de624256c16..9e815925a1f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -26,7 +26,7 @@ gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
-gem 'omniauth-gitlab', '~> 1.0.0'
+gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.7.0'
@@ -137,6 +137,7 @@ gem 'acts-as-taggable-on', '~> 4.0'
gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2'
+gem 'sidekiq-limit_fetch', '~> 3.4'
# HTTP requests
gem 'httparty', '~> 0.13.3'
@@ -152,7 +153,7 @@ gem 'settingslogic', '~> 2.0.9'
gem 'version_sorter', '~> 2.1.0'
# Cache
-gem 'redis-rails', '~> 4.0.0'
+gem 'redis-rails', '~> 5.0.1'
# Redis
gem 'redis', '~> 3.2'
@@ -329,13 +330,10 @@ gem 'octokit', '~> 4.3.0'
gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8'
+gem 'html2text'
gem 'ruby-prof', '~> 0.16.2'
-## CI
-gem 'activerecord-session_store', '~> 1.0.0'
-gem 'nested_form', '~> 0.3.2'
-
# OAuth
gem 'oauth2', '~> 1.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 6ea0578d9d2..bdc60552480 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -32,12 +32,6 @@ GEM
activemodel (= 4.2.7.1)
activesupport (= 4.2.7.1)
arel (~> 6.0)
- activerecord-session_store (1.0.0)
- actionpack (>= 4.0, < 5.1)
- activerecord (>= 4.0, < 5.1)
- multi_json (~> 1.11, >= 1.11.2)
- rack (>= 1.5.2, < 3)
- railties (>= 4.0, < 5.1)
activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5)
activesupport (4.2.7.1)
@@ -345,6 +339,8 @@ GEM
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
+ html2text (0.2.0)
+ nokogiri (~> 1.6)
htmlentities (4.3.4)
httparty (0.13.7)
json (~> 1.8)
@@ -416,7 +412,6 @@ GEM
multi_xml (0.5.5)
multipart-post (2.0.0)
mysql2 (0.3.20)
- nested_form (0.3.2)
net-ldap (0.12.1)
net-ssh (3.0.1)
newrelic_rpm (3.16.0.318)
@@ -456,7 +451,7 @@ GEM
omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-gitlab (1.0.1)
+ omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1)
@@ -573,23 +568,23 @@ GEM
json
redcarpet (3.3.3)
redis (3.2.2)
- redis-actionpack (4.0.1)
- actionpack (~> 4)
- redis-rack (~> 1.5.0)
- redis-store (~> 1.1.0)
- redis-activesupport (4.1.5)
- activesupport (>= 3, < 5)
- redis-store (~> 1.1.0)
+ redis-actionpack (5.0.1)
+ actionpack (>= 4.0, < 6)
+ redis-rack (>= 1, < 3)
+ redis-store (>= 1.1.0, < 1.4.0)
+ redis-activesupport (5.0.1)
+ activesupport (>= 3, < 6)
+ redis-store (~> 1.2.0)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
- redis-rack (1.5.0)
+ redis-rack (1.6.0)
rack (~> 1.5)
- redis-store (~> 1.1.0)
- redis-rails (4.0.0)
- redis-actionpack (~> 4)
- redis-activesupport (~> 4)
- redis-store (~> 1.1.0)
- redis-store (1.1.7)
+ redis-store (~> 1.2.0)
+ redis-rails (5.0.1)
+ redis-actionpack (~> 5.0.0)
+ redis-activesupport (~> 5.0.0)
+ redis-store (~> 1.2.0)
+ redis-store (1.2.0)
redis (>= 2.2)
request_store (1.3.1)
rerun (0.11.0)
@@ -598,7 +593,7 @@ GEM
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.0.6)
+ rouge (2.0.7)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -685,6 +680,8 @@ GEM
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0)
+ sidekiq-limit_fetch (3.4.0)
+ sidekiq (>= 4)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
@@ -807,7 +804,6 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
- activerecord-session_store (~> 1.0.0)
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
@@ -879,6 +875,7 @@ DEPENDENCIES
health_check (~> 2.2.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
+ html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
jira-ruby (~> 1.1.2)
@@ -899,7 +896,6 @@ DEPENDENCIES
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
- nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
newrelic_rpm (~> 3.16)
nokogiri (~> 1.6.7, >= 1.6.7.2)
@@ -913,7 +909,7 @@ DEPENDENCIES
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
- omniauth-gitlab (~> 1.0.0)
+ omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.7.0)
@@ -938,7 +934,7 @@ DEPENDENCIES
redcarpet (~> 3.3.3)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
- redis-rails (~> 4.0.0)
+ redis-rails (~> 5.0.1)
request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0)
@@ -961,6 +957,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0)
+ sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
@@ -994,4 +991,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.5
+ 1.13.6
diff --git a/README.md b/README.md
index dbe6db3ebed..f63543ca39d 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,10 @@ GitLab is a Ruby on Rails application that runs on the following software:
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
+## UX design
+
+Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code.
+
## Third-party applications
There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages.
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 59ac9b9cef5..906a1a69d93 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */
(function() {
this.Activities = (function() {
function Activities() {
@@ -13,12 +13,12 @@
}
Activities.prototype.updateTooltips = function() {
- return gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
+ gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
- return Pager.init(20, true);
+ Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 1ef340e4ca1..31852e4750c 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, padded-blocks, max-len */
(function() {
this.Admin = (function() {
function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1cab66e109e..1c625e2f2b1 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
(function() {
this.Api = {
groupsPath: "/api/:version/groups.json",
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 7d942de0184..76f3c6506ed 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
// 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
@@ -13,7 +13,6 @@
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
/*= require jquery.endless-scroll */
-/*= require jquery.timeago */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
@@ -54,16 +53,34 @@
/*= require_directory ./u2f */
/*= require_directory . */
/*= require fuzzaldrin-plus */
+/*= require es6-promise.auto */
(function () {
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow);
+ // automatically adjust scroll position for hash urls taking the height of the navbar into account
+ // https://github.com/twitter/bootstrap/issues/1768
+ window.adjustScroll = function() {
+ var navbar = document.querySelector('.navbar-gitlab');
+ var subnav = document.querySelector('.layout-nav');
+ var fixedTabs = document.querySelector('.js-tabs-affix');
+
+ adjustment = 0;
+ if (navbar) adjustment -= navbar.offsetHeight;
+ if (subnav) adjustment -= subnav.offsetHeight;
+ if (fixedTabs) adjustment -= fixedTabs.offsetHeight;
+
+ return scrollBy(0, adjustment);
+ };
+
+ window.addEventListener("hashchange", adjustScroll);
+
window.onload = function () {
// Scroll the window to avoid the topnav bar
// https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
- return setTimeout(gl.utils.shiftWindow, 100);
+ return setTimeout(adjustScroll, 100);
}
};
@@ -194,9 +211,6 @@
e.preventDefault();
return new ConfirmDangerModal(form, text);
});
- $document.on('click', 'button', function () {
- return $(this).blur();
- });
$('input[type="search"]').each(function () {
var $this = $(this);
$this.attr('value', $this.val());
@@ -238,8 +252,5 @@
// bind sidebar events
new gl.Sidebar();
-
- // Custom time ago
- gl.utils.shortTimeAgo($('.js-short-timeago'));
});
}).call(this);
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index c7eff27f971..9417afc2ea7 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.Aside = (function() {
function Aside() {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index ab09e4475e6..f45dbe4cbf2 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index d7cda977845..f4302e2e9f6 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */
(function() {
this.AwardsHandler = (function() {
var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index 074378b9e52..a5d62f881fe 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */
/*= require jquery.ba-resize */
/*= require autosize */
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index a64cefb62bd..3998ee9a0a0 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, padded-blocks, max-len */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 7ff88ecdcaf..4edcaa15fe5 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 4ac343f876c..72362988b2e 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 05b213fe3fb..6a49715590c 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
// Toggle button. Show/hide content inside parent container.
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 33fb4f8185c..e0a2e8ac12e 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 */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
(function() {
this.BlobFileDropzone = (function() {
function BlobFileDropzone(form, method) {
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 344fe5dcd94..7e8f1062ab3 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 */
+/* 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, no-undef, padded-blocks, max-len */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 9e992f7913c..9a694daa010 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 */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */
(function() {
this.BlobGitignoreSelectors = (function() {
function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 41a83a56146..9a77fe35d55 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 */
+/* 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, no-undef, padded-blocks, max-len */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index b801c10f168..b8eb0f60a8e 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 */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 60840560dd3..0c74aaaa852 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index efb22d38513..7ba918a05f8 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -22,6 +22,8 @@ $(() => {
gl.IssueBoardsApp.$destroy(true);
}
+ Store.create();
+
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
@@ -37,16 +39,15 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase,
detailIssue: Store.detail
},
- init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
- }
+ },
},
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
- ready () {
+ mounted () {
Store.disabled = this.disabled;
gl.boardService.all()
.then((resp) => {
@@ -60,6 +61,8 @@ $(() => {
}
});
+ this.state.lists = _.sortBy(this.state.lists, 'position');
+
Store.addBlankState();
this.loading = false;
});
@@ -70,6 +73,9 @@ $(() => {
el: '#js-boards-seach',
data: {
filters: Store.state.filters
+ },
+ mounted () {
+ gl.issueBoards.newListDropdownInit();
}
});
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 0e03d43872b..31de3b25284 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -10,6 +10,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
+ template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete,
@@ -24,7 +25,6 @@
return {
detailIssue: Store.detail,
filters: Store.state.filters,
- showIssueForm: false
};
},
watch: {
@@ -58,10 +58,10 @@
},
methods: {
showNewIssueForm() {
- this.showIssueForm = !this.showIssueForm;
+ this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
- ready () {
+ mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
@@ -72,13 +72,9 @@
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
- $board = this.$parent.$refs.board[e.oldIndex + 1],
- list = $board.list;
-
- $board.$destroy(true);
+ list = Store.findList('id', parseInt(e.item.dataset.id));
this.$nextTick(() => {
- Store.state.lists.splice(e.newIndex, 0, list);
Store.moveList(list, order);
});
}
@@ -87,8 +83,5 @@
this.sortable = Sortable.create(this.$el.parentNode, options);
},
- beforeDestroy () {
- Store.state.lists.$remove(this.list);
- }
});
})();
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 885553690d3..691487b272a 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -30,6 +30,8 @@
});
});
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+
// Save the labels
gl.boardService.generateDefaultLists()
.then((resp) => {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 2f6c03e3538..b1afbe7d97e 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -6,6 +6,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
+ template: '#js-board-list-card',
props: {
list: Object,
issue: Object,
@@ -53,11 +54,6 @@
mouseDown () {
this.showDetail = true;
},
- mouseMove () {
- if (this.showDetail) {
- this.showDetail = false;
- }
- },
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 34fc7694241..379f4f0d72b 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -9,6 +9,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({
+ template: '#js-board-list-template',
components: {
'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
@@ -19,20 +20,20 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
- showIssueForm: Boolean
},
data () {
return {
scrollOffset: 250,
filters: Store.state.filters,
- showCount: false
+ showCount: false,
+ showIssueForm: false
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
- this.$els.list.scrollTop = 0;
+ this.$refs.list.scrollTop = 0;
},
deep: true
},
@@ -51,15 +52,20 @@
});
}
},
+ computed: {
+ orderedIssues () {
+ return _.sortBy(this.issues, 'priority');
+ },
+ },
methods: {
listHeight () {
- return this.$els.list.getBoundingClientRect().height;
+ return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
- return this.$els.list.scrollHeight;
+ return this.$refs.list.scrollHeight;
},
scrollTop () {
- return this.$els.list.scrollTop + this.listHeight();
+ return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
const getIssues = this.list.nextPage();
@@ -72,7 +78,7 @@
}
},
},
- ready () {
+ mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
group: 'issues',
sort: false,
@@ -81,23 +87,27 @@
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
+ card.showDetail = false;
Store.moving.issue = card.issue;
Store.moving.list = card.list;
gl.issueBoards.onStart();
},
onAdd: (e) => {
- gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ // Add the element back to original list to allow Vue to handle DOM updates
+ e.from.appendChild(e.item);
+
+ this.$nextTick(() => {
+ // Update the issues once we know the element has been moved
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ });
},
- onRemove: (e) => {
- this.$refs.issue[e.oldIndex].$destroy(true);
- }
});
- this.sortable = Sortable.create(this.$els.list, options);
+ this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
- this.$els.list.onscroll = () => {
+ this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index 7fc0bfd56f3..a7989a2ff4c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -7,7 +7,6 @@
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
- showIssueForm: Boolean
},
data() {
return {
@@ -15,11 +14,6 @@
error: false
};
},
- watch: {
- showIssueForm () {
- this.$els.input.focus();
- }
- },
methods: {
submit(e) {
e.preventDefault();
@@ -37,28 +31,30 @@
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
- $(this.$els.submitButton).enable();
+ $(this.$refs.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
- this.showIssueForm = true;
});
this.cancel();
},
cancel() {
- this.showIssueForm = false;
this.title = '';
+ this.$parent.showIssueForm = false;
}
- }
+ },
+ mounted() {
+ this.$refs.input.focus();
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 4928320d015..d5cb6164e0b 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -41,7 +41,7 @@
this.detail.issue = {};
}
},
- ready () {
+ mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
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 14f618fd5d5..10ce746deb5 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,5 +1,8 @@
/* eslint-disable */
-$(() => {
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => {
@@ -15,54 +18,58 @@ $(() => {
});
});
- $('.js-new-board-list').each(function () {
- const $this = $(this);
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+ gl.issueBoards.newListDropdownInit = () => {
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
- $this.glDropdown({
- data(term, callback) {
- $.get($this.attr('data-labels'))
- .then((resp) => {
- callback(resp);
- });
- },
- 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}`
- });
+ $this.glDropdown({
+ data(term, callback) {
+ $.get($this.attr('data-labels'))
+ .then((resp) => {
+ callback(resp);
+ });
+ },
+ 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}`
+ });
- return $li.append($a.prepend($labelColor));
- },
- search: {
- fields: ['title']
- },
- filterable: true,
- selectable: true,
- multiSelect: true,
- clicked (label, $el, e) {
- e.preventDefault();
+ return $li.append($a.prepend($labelColor));
+ },
+ search: {
+ fields: ['title']
+ },
+ filterable: true,
+ selectable: true,
+ multiSelect: true,
+ clicked (label, $el, e) {
+ e.preventDefault();
- if (!Store.findList('title', label.title)) {
- Store.new({
- title: label.title,
- position: Store.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
+ if (!Store.findList('title', label.title)) {
+ Store.new({
title: label.title,
- color: label.color
- }
- });
+ position: Store.state.lists.length - 2,
+ list_type: 'label',
+ label: {
+ id: label.id,
+ title: label.title,
+ color: label.color
+ }
+ });
+
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
+ }
}
- }
+ });
});
- });
-});
+ };
+})();
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 db9a5a8e40a..5f99de39122 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -23,7 +23,7 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 50,
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index b331a26fed5..8a7dc67409e 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -42,7 +42,8 @@ class List {
}
destroy () {
- gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
+ gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id);
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 175e034afed..6bc95aa60f2 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -39,6 +39,8 @@
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
},
@@ -58,6 +60,8 @@
title: 'Welcome to your Issue Board!',
position: 0
});
+
+ this.state.lists = _.sortBy(this.state.lists, 'position');
},
removeBlankState () {
this.removeList('blank');
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 039ca491cf5..01e09ec482e 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function () {
'use strict';
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 5d4d23e26c6..e7ceb602601 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
(function() {
this.Breakpoints = (function() {
var BreakpointInstance, instance;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index 576f4c76c1e..30432dae278 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
$(function() {
var previewPath;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 12e653f4122..e198306e67a 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, 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, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -8,56 +8,55 @@
Build.state = null;
function Build(options) {
- this.page_url = options.page_url;
- this.build_url = options.build_url;
- this.build_status = options.build_status;
+ options = options || $('.js-build-options').data();
+ this.pageUrl = options.pageUrl;
+ this.buildUrl = options.buildUrl;
+ this.buildStatus = options.buildStatus;
this.state = options.state1;
- this.build_stage = options.build_stage;
- this.hideSidebar = bind(this.hideSidebar, this);
- this.toggleSidebar = bind(this.toggleSidebar, this);
+ this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
clearInterval(Build.interval);
// Init breakpoint checker
this.bp = Breakpoints.get();
+
this.initSidebar();
+ this.$buildScroll = $('#js-build-scroll');
- this.populateJobs(this.build_stage);
- this.updateStageDropdownText(this.build_stage);
+ this.populateJobs(this.buildStage);
+ this.updateStageDropdownText(this.buildStage);
+ this.sidebarOnResize();
- $(window).off('resize.build').on('resize.build', this.hideSidebar);
+ this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
- $('#js-build-scroll > a').off('click').on('click', this.stepTrace);
+ $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
+ $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
if ($('#build-trace').length) {
this.getInitialBuildTrace();
- this.initScrollButtons();
+ this.initScrollButtonAffix();
}
- if (this.build_status === "running" || this.build_status === "pending") {
+ if (this.buildStatus === "running" || this.buildStatus === "pending") {
+ // Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
- return $(this).text("enable autoscroll");
+ return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
- return $(this).text("disable autoscroll");
+ return $(this).text("Disable autoscroll");
}
- //
- // Bind autoscroll button to follow build output
- //
});
Build.interval = setInterval((function(_this) {
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
return function() {
- if (window.location.href.split("#").first() === _this.page_url) {
+ if (_this.location() === _this.pageUrl) {
return _this.getBuildTrace();
}
};
- //
- // Check for new build output if user still watching build page
- // Only valid for runnig build when output changes during time
- //
})(this), 4000);
}
}
@@ -72,20 +71,23 @@
top: this.sidebarTranslationLimits.max
});
this.$sidebar.niceScroll();
- this.hideSidebar();
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
this.$document.off('scroll.translateSidebar').on('scroll.translateSidebar', this.translateSidebar.bind(this));
};
+ Build.prototype.location = function() {
+ return window.location.href.split("#")[0];
+ };
+
Build.prototype.getInitialBuildTrace = function() {
var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
return $.ajax({
- url: this.build_url,
+ url: this.buildUrl,
dataType: 'json',
- success: function(build_data) {
- $('.js-build-output').html(build_data.trace_html);
- if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
+ success: function(buildData) {
+ $('.js-build-output').html(buildData.trace_html);
+ if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
@@ -94,7 +96,7 @@
Build.prototype.getBuildTrace = function() {
return $.ajax({
- url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ url: this.pageUrl + "/trace.json?state=" + (encodeURIComponent(this.state)),
dataType: "json",
success: (function(_this) {
return function(log) {
@@ -108,8 +110,8 @@
$('.js-build-output').html(log.html);
}
return _this.checkAutoscroll();
- } else if (log.status !== _this.build_status) {
- return Turbolinks.visit(_this.page_url);
+ } else if (log.status !== _this.buildStatus) {
+ return Turbolinks.visit(_this.pageUrl);
}
};
})(this)
@@ -122,12 +124,11 @@
}
};
- Build.prototype.initScrollButtons = function() {
- var $body, $buildScroll, $buildTrace;
- $buildScroll = $('#js-build-scroll');
+ Build.prototype.initScrollButtonAffix = function() {
+ var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
- return $buildScroll.affix({
+ return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
@@ -136,18 +137,12 @@
});
};
- Build.prototype.shouldHideSidebar = function() {
+ Build.prototype.shouldHideSidebarForViewport = function() {
var bootstrapBreakpoint;
bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- Build.prototype.toggleSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.toggleClass('right-sidebar-expanded right-sidebar-collapsed');
- }
- };
-
Build.prototype.translateSidebar = function(e) {
var newPosition = this.sidebarTranslationLimits.max - (document.body.scrollTop || document.documentElement.scrollTop);
if (newPosition < this.sidebarTranslationLimits.min) newPosition = this.sidebarTranslationLimits.min;
@@ -156,12 +151,20 @@
});
};
- Build.prototype.hideSidebar = function() {
- if (this.shouldHideSidebar()) {
- return this.$sidebar.removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
- } else {
- return this.$sidebar.removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
- }
+ Build.prototype.toggleSidebar = function(shouldHide) {
+ var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
+ this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
+ .toggleClass('sidebar-collapsed', shouldHide);
+ this.$sidebar.toggleClass('right-sidebar-expanded', shouldShow)
+ .toggleClass('right-sidebar-collapsed', shouldHide);
+ };
+
+ Build.prototype.sidebarOnResize = function() {
+ this.toggleSidebar(this.shouldHideSidebarForViewport());
+ };
+
+ Build.prototype.sidebarOnClick = function() {
+ if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
};
Build.prototype.updateArtifactRemoveDate = function() {
@@ -169,7 +172,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
- return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
+ return $date.text(gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index 49f84581650..c423a548a30 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, padded-blocks, max-len */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index fac5b4f17da..67509ea7d91 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */
(function() {
this.Commit = (function() {
function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 16d63729d31..3f29826fa9b 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index ffddce1297b..4c2ae595319 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.ImageFile = (function() {
var prepareFrames;
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index c765d233831..951fb338f9d 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, 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 */
(function() {
this.CommitsList = (function() {
function CommitsList() {}
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index b3f769d4129..d4243baadb5 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.Compare = (function() {
function Compare(opts) {
@@ -80,7 +80,8 @@
success: function(html) {
loading.hide();
$target.html(html);
- return $('.js-timeago', $target).timeago();
+ var className = '.' + $target[0].className.replace(' ', '.');
+ gl.utils.localTimeAgo($('.js-timeago', className));
}
});
};
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 143d21adb37..686a48486f3 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 7808d7fe313..1cc34e490c2 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
/*= require clipboard */
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 4ddafff428f..00da5f17f9f 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, max-len, one-var, camelcase, one-var-declaration-per-line, no-unused-vars, no-unused-expressions, no-sequences, object-shorthand, comma-dangle, prefer-arrow-callback, semi, radix, padded-blocks, max-len */
(function() {
this.Diff = (function() {
var UNFOLD_COUNT;
@@ -43,10 +43,6 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
- // indent is used to compensate for single space indent to fit
- // '+' and '-' prepended to diff lines,
- // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
- indent: 1,
view: file.data('view')
};
return $.get(link, params, function(response) {
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 29a12a2395b..52e2846d279 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,9 +1,13 @@
/* eslint-disable */
-((w) => {
- w.CommentAndResolveBtn = Vue.extend({
+(() => {
+ const CommentAndResolveBtn = Vue.extend({
props: {
discussionId: String,
- textareaIsEmpty: Boolean
+ },
+ data() {
+ return {
+ textareaIsEmpty: true
+ }
},
computed: {
discussion: function () {
@@ -35,7 +39,7 @@
}
}
},
- ready: function () {
+ mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
@@ -47,4 +51,6 @@
$(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
}
});
+
+ Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index bcc052c7c8c..27af9fc96ad 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable */
-((w) => {
- w.ResolveBtn = Vue.extend({
+(() => {
+ const ResolveBtn = Vue.extend({
props: {
noteId: Number,
discussionId: String,
@@ -54,7 +54,7 @@
},
methods: {
updateTooltip: function () {
- $(this.$els.button)
+ $(this.$refs.button)
.tooltip('hide')
.tooltip('fixTitle');
},
@@ -89,8 +89,8 @@
});
}
},
- compiled: function () {
- $(this.$els.button).tooltip({
+ mounted: function () {
+ $(this.$refs.button).tooltip({
container: 'body'
});
},
@@ -101,4 +101,6 @@
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
}
});
-})(window);
+
+ Vue.component('resolve-btn', ResolveBtn);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 24a99e23132..9522ccb49da 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -13,6 +13,9 @@
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolvedCountText() {
+ return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
}
});
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index 060034f049b..b945a09fcbe 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable */
-((w) => {
- w.ResolveDiscussionBtn = Vue.extend({
+(() => {
+ const ResolveDiscussionBtn = Vue.extend({
props: {
discussionId: String,
mergeRequestId: Number,
@@ -54,4 +54,6 @@
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
}
});
-})(window);
+
+ Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
+})();
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 6149bfd052a..bd4c20aed8b 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -8,24 +8,35 @@
//= require_directory ./components
$(() => {
- window.DiffNotesApp = new Vue({
- el: '#diff-notes-app',
- components: {
- 'resolve-btn': ResolveBtn,
- 'resolve-discussion-btn': ResolveDiscussionBtn,
- 'comment-and-resolve-btn': CommentAndResolveBtn
- },
- methods: {
- compileComponents: function () {
- const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
- if ($components.length) {
- $components.each(function () {
- DiffNotesApp.$compile($(this).get(0));
- });
+ const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
+
+ window.gl = window.gl || {};
+ window.gl.diffNoteApps = {};
+
+ gl.diffNotesCompileComponents = () => {
+ const $components = $(COMPONENT_SELECTOR).filter(function () {
+ return $(this).closest('resolve-count').length !== 1;
+ });
+
+ if ($components) {
+ $components.each(function () {
+ const $this = $(this);
+ const noteId = $this.attr(':note-id');
+ const tmp = Vue.extend({
+ template: $this.get(0).outerHTML
+ });
+ const tmpApp = new tmp().$mount();
+
+ if (noteId) {
+ gl.diffNoteApps[`note_${noteId}`] = tmpApp;
}
- }
+
+ $this.replaceWith(tmpApp.$el);
+ });
}
- });
+ };
+
+ gl.diffNotesCompileComponents();
new Vue({
el: '#resolve-count-app',
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 8e4fd1f19ba..756a24cc0fc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -29,6 +29,9 @@
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:builds:show':
+ new Build();
+ break;
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 1a0aa9757ba..e1e76bca6ad 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */
/*= require preview_markdown */
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
new file mode 100644
index 00000000000..b769161e058
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -0,0 +1,248 @@
+//= require vue
+//= require vue-resource
+//= require_tree ../services/
+//= require ./environment_item
+
+/* globals Vue, EnvironmentsService */
+/* eslint-disable no-param-reassign */
+
+(() => { // eslint-disable-line
+ 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 `filterEnvironmnetsByState`
+ * 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 filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
+ if (item.children) {
+ const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
+ if (filteredChildren.length) {
+ item.children = filteredChildren;
+ return item;
+ }
+ }
+ return fn(item);
+ }).filter(Boolean);
+
+ window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'environment-item': window.gl.environmentsList.EnvironmentItem,
+ },
+
+ data() {
+ const environmentsData = document.querySelector('#environments-list-view').dataset;
+
+ return {
+ state: this.store.state,
+ visibility: 'available',
+ isLoading: false,
+ cssContainerClass: environmentsData.cssClass,
+ endpoint: environmentsData.environmentsDataEndpoint,
+ canCreateDeployment: environmentsData.canCreateDeployment,
+ canReadEnvironment: environmentsData.canReadEnvironment,
+ canCreateEnvironment: environmentsData.canCreateEnvironment,
+ projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
+ projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
+ newEnvironmentPath: environmentsData.newEnvironmentPath,
+ helpPagePath: environmentsData.helpPagePath,
+ };
+ },
+
+ computed: {
+ filteredEnvironments() {
+ return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
+ },
+
+ scope() {
+ return this.$options.getQueryParameter('scope');
+ },
+
+ canReadEnvironmentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
+ },
+
+ canCreateDeploymentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
+ },
+
+ canCreateEnvironmentParsed() {
+ return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
+ },
+ },
+
+ /**
+ * Fetches all the environmnets and stores them.
+ * Toggles loading property.
+ */
+ created() {
+ gl.environmentsService = new EnvironmentsService(this.endpoint);
+
+ const scope = this.$options.getQueryParameter('scope');
+ if (scope) {
+ this.visibility = scope;
+ }
+
+ this.isLoading = true;
+
+ return gl.environmentsService.all()
+ .then(resp => resp.json())
+ .then((json) => {
+ this.store.storeEnvironments(json);
+ this.isLoading = false;
+ });
+ },
+
+ /**
+ * Transforms the url parameter into an object and
+ * returns the one requested.
+ *
+ * @param {String} param
+ * @returns {String} The value of the requested parameter.
+ */
+ getQueryParameter(parameter) {
+ return window.location.search.substring(1).split('&').reduce((acc, param) => {
+ const paramSplited = param.split('=');
+ acc[paramSplited[0]] = paramSplited[1];
+ return acc;
+ }, {})[parameter];
+ },
+
+ /**
+ * Converts permission provided as strings to booleans.
+ * @param {String} string
+ * @returns {Boolean}
+ */
+ convertPermissionToBoolean(string) {
+ return string === 'true';
+ },
+
+ methods: {
+ toggleRow(model) {
+ return this.store.toggleFolder(model.name);
+ },
+ },
+
+ template: `
+ <div :class="cssContainerClass">
+ <div class="top-area">
+ <ul v-if="!isLoading" class="nav-links">
+ <li v-bind:class="{ 'active': scope === undefined }">
+ <a :href="projectEnvironmentsPath">
+ Available
+ <span
+ class="badge js-available-environments-count"
+ v-html="state.availableCounter"></span>
+ </a>
+ </li>
+ <li v-bind:class="{ 'active' : scope === 'stopped' }">
+ <a :href="projectStoppedEnvironmentsPath">
+ Stopped
+ <span
+ class="badge js-stopped-environments-count"
+ v-html="state.stoppedCounter"></span>
+ </a>
+ </li>
+ </ul>
+ <div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
+ <a :href="newEnvironmentPath" class="btn btn-create">
+ New environment
+ </a>
+ </div>
+ </div>
+
+ <div class="environments-container">
+ <div class="environments-list-loading text-center" v-if="isLoading">
+ <i class="fa fa-spinner spin"></i>
+ </div>
+
+ <div
+ class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.environments.length === 0">
+ <h2 class="blank-state-title">
+ You don't have any environments right now.
+ </h2>
+ <p class="blank-state-text">
+ Environments are places where code gets deployed, such as staging or production.
+ <br />
+ <a :href="helpPagePath">
+ Read more about environments
+ </a>
+ </p>
+
+ <a
+ v-if="canCreateEnvironmentParsed"
+ :href="newEnvironmentPath"
+ class="btn btn-create">
+ New Environment
+ </a>
+ </div>
+
+ <div
+ class="table-holder"
+ v-if="!isLoading && state.environments.length > 0">
+ <table class="table ci-table environments">
+ <thead>
+ <tr>
+ <th>Environment</th>
+ <th>Last deployment</th>
+ <th>Build</th>
+ <th>Commit</th>
+ <th></th>
+ <th class="hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in filteredEnvironments"
+ v-bind:model="model">
+
+ <tr
+ is="environment-item"
+ :model="model"
+ :toggleRow="toggleRow.bind(model)"
+ :can-create-deployment="canCreateDeploymentParsed"
+ :can-read-environment="canReadEnvironmentParsed"></tr>
+
+ <tr v-if="model.isOpen && model.children && model.children.length > 0"
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :toggleRow="toggleRow.bind(children)">
+ </tr>
+
+ </template>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
new file mode 100644
index 00000000000..edd39c02a46
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -0,0 +1,67 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
+ props: {
+ actions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+
+ /**
+ * Appends the svg icon that were render in the index page.
+ * In order to reuse the svg instead of copy and paste in this template
+ * we need to render it outside this component using =custom_icon partial.
+ *
+ * TODO: Remove this when webpack is merged.
+ *
+ */
+ mounted() {
+ const playIcon = document.querySelector('.play-icon-svg.hidden svg');
+
+ const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container');
+ const actionContainers = this.$el.querySelectorAll('.action-play-icon-container');
+ // Phantomjs does not have support to iterate a nodelist.
+ const actionsArray = [].slice.call(actionContainers);
+
+ if (playIcon && actionsArray && dropdownContainer) {
+ dropdownContainer.appendChild(playIcon.cloneNode(true));
+
+ actionsArray.forEach((element) => {
+ element.appendChild(playIcon.cloneNode(true));
+ });
+ }
+ },
+
+ template: `
+ <div class="inline">
+ <div class="dropdown">
+ <a class="dropdown-new btn btn-default" data-toggle="dropdown">
+ <span class="dropdown-play-icon-container">
+ </span>
+ <i class="fa fa-caret-down"></i>
+ </a>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <a :href="action.play_path"
+ data-method="post"
+ rel="nofollow"
+ class="js-manual-action-link">
+ <span class="action-play-icon-container">
+ </span>
+ <span v-html="action.name"></span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
new file mode 100644
index 00000000000..79cd5ded5bd
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -0,0 +1,22 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
+ props: {
+ external_url: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a class="btn external_url" :href="external_url" target="_blank">
+ <i class="fa fa-external-link"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
new file mode 100644
index 00000000000..2f7d1d2a177
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -0,0 +1,494 @@
+/*= require lib/utils/timeago */
+/*= require lib/utils/text_utility */
+/*= require vue_common_component/commit */
+/*= require ./environment_actions */
+/*= require ./environment_external_url */
+/*= require ./environment_stop */
+/*= require ./environment_rollback */
+
+/* globals Vue, timeago */
+
+(() => {
+ /**
+ * Envrionment Item Component
+ *
+ * Used in a hierarchical structure to show folders with children
+ * in a table.
+ * Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html)
+ *
+ * See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539)
+ * for more information.15
+ */
+
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
+
+ components: {
+ 'commit-component': window.gl.CommitComponent,
+ 'actions-component': window.gl.environmentsList.ActionsComponent,
+ 'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
+ 'stop-component': window.gl.environmentsList.StopComponent,
+ 'rollback-component': window.gl.environmentsList.RollbackComponent,
+ },
+
+ props: {
+ model: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ toggleRow: {
+ type: Function,
+ required: false,
+ },
+
+ canCreateDeployment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ canReadEnvironment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ data() {
+ return {
+ rowClass: {
+ 'children-row': this.model['vue-isChildren'],
+ },
+ };
+ },
+
+ computed: {
+
+ /**
+ * If an item has a `children` entry it means it is a folder.
+ * Folder items have different behaviours - it is possible to toggle
+ * them and show their children.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isFolder() {
+ return this.model.children && this.model.children.length > 0;
+ },
+
+ /**
+ * If an item is inside a folder structure will return true.
+ * Used for css purposes.
+ *
+ * @returns {Boolean|undefined}
+ */
+ isChildren() {
+ return this.model['vue-isChildren'];
+ },
+
+ /**
+ * Counts the number of environments in each folder.
+ * Used to show a badge with the counter.
+ *
+ * @returns {Number|Undefined} The number of environments for the current folder.
+ */
+ childrenCounter() {
+ return this.model.children && this.model.children.length;
+ },
+
+ /**
+ * Verifies if `last_deployment` key exists in the current Envrionment.
+ * This key is required to render most of the html - this method works has
+ * an helper.
+ *
+ * @returns {Boolean}
+ */
+ hasLastDeploymentKey() {
+ if (this.model.last_deployment &&
+ !this.$options.isObjectEmpty(this.model.last_deployment)) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Verifies is the given environment has manual actions.
+ * Used to verify if we should render them or nor.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ hasManualActions() {
+ return this.model.last_deployment && this.model.last_deployment.manual_actions &&
+ this.model.last_deployment.manual_actions.length > 0;
+ },
+
+ /**
+ * Returns the value of the `stoppable?` key provided in the response.
+ *
+ * @returns {Boolean}
+ */
+ isStoppable() {
+ return this.model['stoppable?'];
+ },
+
+ /**
+ * Verifies if the `deployable` key is present in `last_deployment` key.
+ * Used to verify whether we should or not render the rollback partial.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ canRetry() {
+ return this.hasLastDeploymentKey &&
+ this.model.last_deployment &&
+ this.model.last_deployment.deployable;
+ },
+
+ /**
+ * Human readable date.
+ *
+ * @returns {String}
+ */
+ createdDate() {
+ const timeagoInstance = new timeago(); // eslint-disable-line
+
+ return timeagoInstance.format(this.model.created_at);
+ },
+
+ /**
+ * Returns the manual actions with the name parsed.
+ *
+ * @returns {Array.<Object>|Undefined}
+ */
+ manualActions() {
+ if (this.hasManualActions) {
+ return this.model.last_deployment.manual_actions.map((action) => {
+ const parsedAction = {
+ name: gl.text.humanize(action.name),
+ play_path: action.play_path,
+ };
+ return parsedAction;
+ });
+ }
+ return [];
+ },
+
+ /**
+ * Builds the string used in the user image alt attribute.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.user &&
+ this.model.last_deployment.user.username) {
+ return `${this.model.last_deployment.user.username}'s avatar'`;
+ }
+ return '';
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.tag) {
+ return this.model.last_deployment.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.model.last_deployment && this.model.last_deployment.ref) {
+ return this.model.last_deployment.ref;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.commit_path) {
+ return this.model.last_deployment.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.short_id) {
+ return this.model.last_deployment.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.title) {
+ return this.model.last_deployment.commit.title;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.commit &&
+ this.model.last_deployment.commit.author) {
+ return this.model.last_deployment.commit.author;
+ }
+
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `retry_path` key is present and returns its value.
+ *
+ * @returns {String|Undefined}
+ */
+ retryUrl() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.deployable &&
+ this.model.last_deployment.deployable.retry_path) {
+ return this.model.last_deployment.deployable.retry_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * Verifies if the `last?` key is present and returns its value.
+ *
+ * @returns {Boolean|Undefined}
+ */
+ isLastDeployment() {
+ return this.model.last_deployment && this.model.last_deployment['last?'];
+ },
+
+ /**
+ * Builds the name of the builds needed to display both the name and the id.
+ *
+ * @returns {String}
+ */
+ buildName() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.deployable) {
+ return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
+ }
+ return '';
+ },
+
+ /**
+ * Builds the needed string to show the internal id.
+ *
+ * @returns {String}
+ */
+ deploymentInternalId() {
+ if (this.model.last_deployment &&
+ this.model.last_deployment.iid) {
+ return `#${this.model.last_deployment.iid}`;
+ }
+ return '';
+ },
+
+ /**
+ * Verifies if the user object is present under last_deployment object.
+ *
+ * @returns {Boolean}
+ */
+ deploymentHasUser() {
+ return !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user);
+ },
+
+ /**
+ * Returns the user object nested with the last_deployment object.
+ * Used to render the template.
+ *
+ * @returns {Object}
+ */
+ deploymentUser() {
+ if (!this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
+ return this.model.last_deployment.user;
+ }
+ return {};
+ },
+
+ /**
+ * Verifies if the build name column should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderBuildName() {
+ return !this.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
+ },
+
+ /**
+ * Verifies if deplyment internal ID should be rendered by verifing
+ * if all the information needed is present
+ * and if the environment is not a folder.
+ *
+ * @returns {Boolean}
+ */
+ shouldRenderDeploymentID() {
+ return !this.isFolder &&
+ !this.$options.isObjectEmpty(this.model.last_deployment) &&
+ this.model.last_deployment.iid !== undefined;
+ },
+ },
+
+ /**
+ * Helper to verify if certain given object are empty.
+ * Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
+ * @param {Object} object
+ * @returns {Bollean}
+ */
+ isObjectEmpty(object) {
+ for (const key in object) { // eslint-disable-line
+ if (hasOwnProperty.call(object, key)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ template: `
+ <tr>
+ <td v-bind:class="{ 'children-row': isChildren}">
+ <a
+ v-if="!isFolder"
+ class="environment-name"
+ :href="model.environment_path"
+ v-html="model.name">
+ </a>
+ <span v-else v-on:click="toggleRow(model)" class="folder-name">
+ <span class="folder-icon">
+ <i v-show="model.isOpen" class="fa fa-caret-down"></i>
+ <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
+ </span>
+
+ <span v-html="model.name"></span>
+
+ <span class="badge" v-html="childrenCounter"></span>
+ </span>
+ </td>
+
+ <td class="deployment-column">
+ <span
+ v-if="shouldRenderDeploymentID"
+ v-html="deploymentInternalId">
+ </span>
+
+ <span v-if="!isFolder && deploymentHasUser">
+ by
+ <a :href="deploymentUser.web_url" class="js-deploy-user-container">
+ <img class="avatar has-tooltip s20"
+ :src="deploymentUser.avatar_url"
+ :alt="userImageAltDescription"
+ :title="deploymentUser.username" />
+ </a>
+ </span>
+ </td>
+
+ <td>
+ <a v-if="shouldRenderBuildName"
+ class="build-link"
+ :href="model.last_deployment.deployable.build_path"
+ v-html="buildName">
+ </a>
+ </td>
+
+ <td>
+ <div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
+ <commit-component
+ :tag="commitTag"
+ :ref="commitRef"
+ :commit_url="commitUrl"
+ :short_sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor">
+ </commit-component>
+ </div>
+ <p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
+ No deployments yet
+ </p>
+ </td>
+
+ <td>
+ <span
+ v-if="!isFolder && model.last_deployment"
+ class="environment-created-date-timeago"
+ v-html="createdDate">
+ </span>
+ </td>
+
+ <td class="hidden-xs">
+ <div v-if="!isFolder">
+ <div v-if="hasManualActions && canCreateDeployment"
+ class="inline js-manual-actions-container">
+ <actions-component
+ :actions="manualActions">
+ </actions-component>
+ </div>
+
+ <div v-if="model.external_url && canReadEnvironment"
+ class="inline js-external-url-container">
+ <external-url-component
+ :external_url="model.external_url">
+ </external_url-component>
+ </div>
+
+ <div v-if="isStoppable && canCreateDeployment"
+ class="inline js-stop-component-container">
+ <stop-component
+ :stop_url="model.stop_path">
+ </stop-component>
+ </div>
+
+ <div v-if="canRetry && canCreateDeployment"
+ class="inline js-rollback-component-container">
+ <rollback-component
+ :is_last_deployment="isLastDeployment"
+ :retry_url="retryUrl">
+ </rollback-component>
+ </div>
+ </div>
+ </td>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
new file mode 100644
index 00000000000..55e5c826e07
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -0,0 +1,31 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
+ props: {
+ retry_url: {
+ type: String,
+ default: '',
+ },
+ is_last_deployment: {
+ type: Boolean,
+ default: true,
+ },
+ },
+
+ template: `
+ <a class="btn" :href="retry_url" data-method="post" rel="nofollow">
+ <span v-if="is_last_deployment">
+ Re-deploy
+ </span>
+ <span v-else>
+ Rollback
+ </span>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
new file mode 100644
index 00000000000..2c732e50180
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -0,0 +1,27 @@
+/*= require vue */
+/* global Vue */
+
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
+ props: {
+ stop_url: {
+ type: String,
+ default: '',
+ },
+ },
+
+ template: `
+ <a
+ class="btn stop-env-link"
+ :href="stop_url"
+ data-confirm="Are you sure you want to stop this environment?"
+ data-method="post"
+ rel="nofollow">
+ <i class="fa fa-stop stop-env-icon"></i>
+ </a>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
new file mode 100644
index 00000000000..20eee7976ec
--- /dev/null
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -0,0 +1,21 @@
+//= require vue
+//= require_tree ./stores/
+//= require ./components/environment
+//= require ./vue_resource_interceptor
+
+
+$(() => {
+ window.gl = window.gl || {};
+
+ if (window.gl.EnvironmentsListApp) {
+ window.gl.EnvironmentsListApp.$destroy(true);
+ }
+ const Store = window.gl.environmentsList.EnvironmentsStore;
+
+ window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ propsData: {
+ store: Store.create(),
+ },
+ });
+});
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
new file mode 100644
index 00000000000..15ec7b76c3d
--- /dev/null
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -0,0 +1,22 @@
+/* globals Vue */
+/* eslint-disable no-unused-vars, no-param-reassign */
+class EnvironmentsService {
+
+ constructor(root) {
+ Vue.http.options.root = root;
+
+ this.environments = Vue.resource(root);
+
+ Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+ });
+ }
+
+ all() {
+ return this.environments.get();
+ }
+}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
new file mode 100644
index 00000000000..0204a903ab5
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -0,0 +1,131 @@
+/* eslint-disable no-param-reassign */
+(() => {
+ window.gl = window.gl || {};
+ window.gl.environmentsList = window.gl.environmentsList || {};
+
+ gl.environmentsList.EnvironmentsStore = {
+ state: {},
+
+ create() {
+ this.state.environments = [];
+ this.state.stoppedCounter = 0;
+ this.state.availableCounter = 0;
+
+ return this;
+ },
+
+ /**
+ * In order to display a tree view we need to modify the received
+ * data in to a tree structure based on `environment_type`
+ * sorted alphabetically.
+ * In each children a `vue-` property will be added. This property will be
+ * used to know if an item is a children mostly for css purposes. This is
+ * needed because the children row is a fragment instance and therfore does
+ * not accept non-prop attributes.
+ *
+ *
+ * @example
+ * it will transform this:
+ * [
+ * { name: "environment", environment_type: "review" },
+ * { name: "environment_1", environment_type: null }
+ * { name: "environment_2, environment_type: "review" }
+ * ]
+ * into this:
+ * [
+ * { name: "review", children:
+ * [
+ * { name: "environment", environment_type: "review", vue-isChildren: true},
+ * { name: "environment_2", environment_type: "review", vue-isChildren: true}
+ * ]
+ * },
+ * {name: "environment_1", environment_type: null}
+ * ]
+ *
+ *
+ * @param {Array} environments List of environments.
+ * @returns {Array} Tree structured array with the received environments.
+ */
+ storeEnvironments(environments = []) {
+ this.state.stoppedCounter = this.countByState(environments, 'stopped');
+ this.state.availableCounter = this.countByState(environments, 'available');
+
+ const environmentsTree = environments.reduce((acc, environment) => {
+ if (environment.environment_type !== null) {
+ const occurs = acc.filter(element => element.children &&
+ element.name === environment.environment_type);
+
+ environment['vue-isChildren'] = true;
+
+ if (occurs.length) {
+ acc[acc.indexOf(occurs[0])].children.push(environment);
+ acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
+ } else {
+ acc.push({
+ name: environment.environment_type,
+ children: [environment],
+ isOpen: false,
+ 'vue-isChildren': environment['vue-isChildren'],
+ });
+ }
+ } else {
+ acc.push(environment);
+ }
+
+ return acc;
+ }, []).sort(this.sortByName);
+
+ this.state.environments = environmentsTree;
+
+ return environmentsTree;
+ },
+
+ /**
+ * Toggles folder open property given the environment type.
+ *
+ * @param {String} envType
+ * @return {Array}
+ */
+ toggleFolder(envType) {
+ const environments = this.state.environments;
+
+ const environmentsCopy = environments.map((env) => {
+ if (env['vue-isChildren'] && env.name === envType) {
+ env.isOpen = !env.isOpen;
+ }
+
+ return env;
+ });
+
+ this.state.environments = environmentsCopy;
+
+ return environmentsCopy;
+ },
+
+ /**
+ * Given an array of environments, returns the number of environments
+ * that have the given state.
+ *
+ * @param {Array} environments
+ * @param {String} state
+ * @returns {Number}
+ */
+ countByState(environments, state) {
+ return environments.filter(env => env.state === state).length;
+ },
+
+ /**
+ * Sorts the two objects provided by their name.
+ *
+ * @param {Object} a
+ * @param {Object} b
+ * @returns {Number}
+ */
+ sortByName(a, b) {
+ const nameA = a.name.toUpperCase();
+ const nameB = b.name.toUpperCase();
+
+ return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
+ },
+ };
+})();
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..406bdbc1c7d
--- /dev/null
+++ b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
@@ -0,0 +1,12 @@
+/* global Vue */
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ next((response) => {
+ if (typeof response.data === 'string') {
+ response.data = JSON.parse(response.data); // eslint-disable-line
+ }
+
+ Vue.activeResources--; // eslint-disable-line
+ });
+});
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
index 4c9e219aa43..fc6c130113d 100644
--- a/app/assets/javascripts/extensions/array.js
+++ b/app/assets/javascripts/extensions/array.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */
Array.prototype.first = function() {
return this[0];
}
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index 623a80b7053..cdedc865d1b 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, padded-blocks, max-len */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 732136f1f2c..0122e847161 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 46e272c3311..804d7d9c4ab 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.Flash = (function() {
var hideFlash;
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 824413bf20f..5d9ac4d350a 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -16,7 +16,7 @@
},
// Team Members
Members: {
- template: '<li>${username} <small>${title}</small></li>'
+ template: '<li>${avatarTag} ${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
@@ -34,6 +34,8 @@
},
DefaultOptions: {
sorter: function(query, items, searchKey) {
+ // Highlight first item only if at least one char was typed
+ this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
if ((items[0].name != null) && items[0].name === 'loading') {
return items;
}
@@ -110,13 +112,14 @@
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
data: ['loading'],
+ alwaysHighlightFirst: true,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(members) {
return $.map(members, function(m) {
- var title;
+ let title = '';
if (m.username == null) {
return m;
}
@@ -124,8 +127,14 @@
if (m.count) {
title += " (" + m.count + ")";
}
+
+ const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase();
+ const imgAvatar = `<img src="${m.avatar_url}" alt="${m.username}" class="avatar avatar-inline center s26"/>`;
+ const txtAvatar = `<div class="avatar center avatar-inline s26">${autoCompleteAvatar}</div>`;
+
return {
username: m.username,
+ avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
title: gl.utils.sanitize(title),
search: gl.utils.sanitize(m.username + " " + m.name)
};
@@ -182,6 +191,7 @@
insertTpl: '${atwho-at}"${title}"',
data: ['loading'],
callbacks: {
+ sorter: this.DefaultOptions.sorter,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
@@ -236,6 +246,7 @@
displayTpl: this.Labels.template,
insertTpl: '${atwho-at}${title}',
callbacks: {
+ sorter: this.DefaultOptions.sorter,
beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 98e43c4d088..969778dded7 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, 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, max-len */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -249,7 +249,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input) {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index ce54c34492d..db5d9e75b3a 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */
(function() {
this.GLForm = (function() {
function GLForm(form) {
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index e103748d499..32c26349da0 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
// 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
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index b796a9abb49..3273bf3a263 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, padded-blocks, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 818bff0c413..c3a132b3c75 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 */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index dea26a3f1e1..cb2448e8cc7 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, newline-per-chained-call, no-else-return, max-len */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 362a77e868f..051ff98c774 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 */
+/* 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 */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index 774477dc7a9..17a76168a79 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
new file mode 100644
index 00000000000..eea6cd40859
--- /dev/null
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -0,0 +1,53 @@
+/* eslint-disable */
+(function(global) {
+ class GroupLabelSubscription {
+ constructor(container) {
+ const $container = $(container);
+ this.$dropdown = $container.find('.dropdown');
+ this.$subscribeButtons = $container.find('.js-subscribe-button');
+ this.$unsubscribeButtons = $container.find('.js-unsubscribe-button');
+
+ this.$subscribeButtons.on('click', this.subscribe.bind(this));
+ this.$unsubscribeButtons.on('click', this.unsubscribe.bind(this));
+ }
+
+ unsubscribe(event) {
+ event.preventDefault();
+
+ const url = this.$unsubscribeButtons.attr('data-url');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ this.$unsubscribeButtons.removeAttr('data-url');
+ });
+ }
+
+ subscribe(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const url = $btn.attr('data-url');
+
+ this.$unsubscribeButtons.attr('data-url', url);
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ this.toggleSubscriptionButtons();
+ });
+ }
+
+ toggleSubscriptionButtons() {
+ this.$dropdown.toggleClass('hidden');
+ this.$subscribeButtons.toggleClass('hidden');
+ this.$unsubscribeButtons.toggleClass('hidden');
+ }
+ }
+
+ global.GroupLabelSubscription = GroupLabelSubscription;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e3c39c895ba..3dc6f05ca20 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
(function() {
var slice = [].slice;
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 81fcaf06430..c7cbf9ca44b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */
(function() {
$(document).on('todo:toggle', function(e, count) {
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index c53f7c88aa2..9425b6ed9d4 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
(function() {
this.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 8fc498be27d..46503c290ae 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -10,6 +10,7 @@
Issuable.initSearch();
Issuable.initChecks();
Issuable.initResetFilters();
+ Issuable.resetIncomingEmailToken();
return Issuable.initLabelFilterRemove();
},
initTemplates: function() {
@@ -154,6 +155,27 @@
this.issuableBulkActions.willUpdateLabels = false;
}
return true;
+ },
+
+ resetIncomingEmailToken: function() {
+ $('.incoming-email-token-reset').on('click', function(e) {
+ e.preventDefault();
+
+ $.ajax({
+ type: 'PUT',
+ url: $('.incoming-email-token-reset').attr('href'),
+ dataType: 'json',
+ success: function(response) {
+ $('#issue_email').val(response.new_issue_address).focus();
+ },
+ beforeSend: function() {
+ $('.incoming-email-token-reset').text('resetting...');
+ },
+ complete: function() {
+ $('.incoming-email-token-reset').text('reset it');
+ }
+ });
+ });
}
};
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index fae49ee6144..317818951fd 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 849b45756ee..50fdbc89c7c 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 67ace697936..8540b199aba 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
/*= require flash */
/*= require jquery.waitforimages */
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index d7262e5eb74..b39d8274e13 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index 3033e8ca5c2..10de13c9a8a 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c334e3e0c02..812d5cde685 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable 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, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 6b4edf02f4d..2b700539c2b 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var hideEndFade;
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index e1dfdae97de..d8ad5aaeffe 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require Chart */
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 155e30cc462..5221f85ba7a 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require cropper */
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 0c9c2787077..57e7986576c 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require d3 */
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index cc445db274b..5a9a501efe3 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require raphael */
/*= require g.raphael */
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index a68edab2aad..83957af94f3 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
(function(w) {
if (w.gl == null) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8447421195d..d83c41fae9d 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
(function(w) {
var base;
@@ -119,7 +119,6 @@
parser.href = url;
return parser;
};
-
gl.utils.cleanupBeforeFetch = function() {
// Unbind scroll events
$(document).off('scroll');
@@ -127,23 +126,10 @@
$('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
};
- return jQuery.timefor = function(time, suffix, expiredLabel) {
- var suffixFromNow, timefor;
- if (!time) {
- return '';
- }
- suffix || (suffix = 'remaining');
- expiredLabel || (expiredLabel = 'Past due');
- jQuery.timeago.settings.allowFuture = true;
- suffixFromNow = jQuery.timeago.settings.strings.suffixFromNow;
- jQuery.timeago.settings.strings.suffixFromNow = suffix;
- timefor = $.timeago(time);
- if (timefor.indexOf('ago') > -1) {
- timefor = expiredLabel;
- }
- jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow;
- return timefor;
+ gl.utils.isMetaKey = function(e) {
+ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
+
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 59e526ed623..d480fdc882b 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 */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-undef, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
(function() {
(function(w) {
var base;
@@ -22,51 +22,64 @@
if (setTimeago == null) {
setTimeago = true;
}
+
$timeagoEls.each(function() {
- var $el;
- $el = $(this);
- return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+ var $el = $(this);
+ $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+
+ if (setTimeago) {
+ // Recreate with custom template
+ $el.tooltip({
+ template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
+ });
+ }
+ gl.utils.renderTimeago($el);
});
- if (setTimeago) {
- $timeagoEls.timeago();
- $timeagoEls.tooltip('destroy');
- // Recreate with custom template
- return $timeagoEls.tooltip({
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- });
- }
};
- w.gl.utils.shortTimeAgo = function($el) {
- var shortLocale, tmpLocale;
- shortLocale = {
- prefixAgo: null,
- prefixFromNow: null,
- suffixAgo: 'ago',
- suffixFromNow: 'from now',
- seconds: '1 min',
- minute: '1 min',
- minutes: '%d mins',
- hour: '1 hr',
- hours: '%d hrs',
- day: '1 day',
- days: '%d days',
- month: '1 month',
- months: '%d months',
- year: '1 year',
- years: '%d years',
- wordSeparator: ' ',
- numbers: []
+ w.gl.utils.getTimeago = function() {
+ var locale = function(number, index) {
+ return [
+ ['less than a minute ago', 'a while'],
+ ['less than a minute ago', 'in %s seconds'],
+ ['about a minute ago', 'in 1 minute'],
+ ['%s minutes ago', 'in %s minutes'],
+ ['about an hour ago', 'in 1 hour'],
+ ['about %s hours ago', 'in %s hours'],
+ ['a day ago', 'in 1 day'],
+ ['%s days ago', 'in %s days'],
+ ['a week ago', 'in 1 week'],
+ ['%s weeks ago', 'in %s weeks'],
+ ['a month ago', 'in 1 month'],
+ ['%s months ago', 'in %s months'],
+ ['a year ago', 'in 1 year'],
+ ['%s years ago', 'in %s years']
+ ][index];
};
- tmpLocale = $.timeago.settings.strings;
- $el.each(function(el) {
- var $el1;
- $el1 = $(this);
- return $el1.attr('title', gl.utils.formatDate($el.attr('datetime')));
- });
- $.timeago.settings.strings = shortLocale;
- $el.timeago();
- $.timeago.settings.strings = tmpLocale;
+
+ timeago.register('gl_en', locale);
+ return timeago();
+ };
+
+ w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
+ var timefor;
+ if (!time) {
+ return '';
+ }
+ suffix || (suffix = 'remaining');
+ expiredLabel || (expiredLabel = 'Past due');
+ timefor = gl.utils.getTimeago().format(time).replace('in', '');
+ if (timefor.indexOf('ago') > -1) {
+ timefor = expiredLabel;
+ } else {
+ timefor = timefor.trim() + ' ' + suffix;
+ }
+ return timefor;
+ };
+
+ w.gl.utils.renderTimeago = function($element) {
+ var timeagoInstance = gl.utils.getTimeago();
+ timeagoInstance.render($element, 'gl_en');
};
w.gl.utils.getDayDifference = function(a, b) {
@@ -75,7 +88,7 @@
var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((date2 - date1) / millisecondsPerDay);
- }
+ };
})(window);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index dafc006d2e5..d0fe69260a5 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 98f9815ff05..ac44b81ee22 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
(function(w) {
var base;
@@ -112,6 +112,9 @@
gl.text.removeListeners = function(form) {
return $('.js-md', form).off();
};
+ 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)) + '...';
};
diff --git a/app/assets/javascripts/lib/utils/timeago.js b/app/assets/javascripts/lib/utils/timeago.js
new file mode 100644
index 00000000000..edf0a612374
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/timeago.js
@@ -0,0 +1,239 @@
+/* eslint-disable no-unused-expressions, wrap-iife, func-names, curly, no-param-reassign, no-trailing-spaces, prefer-arrow-callback, no-var, one-var, quote-props, space-before-function-paren, vars-on-top, radix, prefer-template, space-infix-ops, no-use-before-define, newline-per-chained-call, no-useless-escape, no-nested-ternary, indent, no-undef, no-plusplus, one-var-declaration-per-line, operator-assignment, consistent-return, keyword-spacing, max-len, space-unary-ops, no-shadow, no-restricted-syntax, guard-for-in, eol-last, max-len */
+
+/**
+ * Copyright (c) 2016 hustcc
+ * License: MIT
+ * Version: v2.0.2
+ * https://github.com/hustcc/timeago.js
+ * This is a forked from (https://gitlab.com/ClemMakesApps/timeago.js)
+**/
+/* eslint-disable */
+/* jshint expr: true */
+!function (root, factory) {
+ if (typeof module === 'object' && module.exports)
+ module.exports = factory(root);
+ else
+ root.timeago = factory(root);
+}(typeof window !== 'undefined' ? window : this,
+function () {
+ var cnt = 0, // the timer counter, for timer key
+ indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),
+
+ // build-in locales: en & zh_CN
+ locales = {
+ 'en': function(number, index) {
+ if (index === 0) return ['just now', 'right now'];
+ var unit = indexMapEn[parseInt(index / 2)];
+ if (number > 1) unit += 's';
+ return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];
+ },
+ },
+ // second, minute, hour, day, week, month, year(365 days)
+ SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],
+ SEC_ARRAY_LEN = 6,
+ ATTR_DATETIME = 'datetime';
+
+ // format Date / string / timestamp to Date instance.
+ function toDate(input) {
+ if (input instanceof Date) return input;
+ if (!isNaN(input)) return new Date(toInt(input));
+ if (/^\d+$/.test(input)) return new Date(toInt(input, 10));
+ input = (input || '').trim().replace(/\.\d+/, '') // remove milliseconds
+ .replace(/-/, '/').replace(/-/, '/')
+ .replace(/T/, ' ').replace(/Z/, ' UTC')
+ .replace(/([\+\-]\d\d)\:?(\d\d)/, ' $1$2'); // -04:00 -> -0400
+ return new Date(input);
+ }
+ // change f into int, remove Decimal. just for code compression
+ function toInt(f) {
+ return parseInt(f);
+ }
+ // format the diff second to *** time ago, with setting locale
+ function formatDiff(diff, locale, defaultLocale) {
+ // if locale is not exist, use defaultLocale.
+ // if defaultLocale is not exist, use build-in `en`.
+ // be sure of no error when locale is not exist.
+ locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');
+ // if (! locales[locale]) locale = defaultLocale;
+ var i = 0;
+ agoin = diff < 0 ? 1 : 0; // timein or timeago
+ diff = Math.abs(diff);
+
+ for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+ diff /= SEC_ARRAY[i];
+ }
+ diff = toInt(diff);
+ i *= 2;
+
+ if (diff > (i === 0 ? 9 : 1)) i += 1;
+ return locales[locale](diff, i)[agoin].replace('%s', diff);
+ }
+ // calculate the diff second between date to be formated an now date.
+ function diffSec(date, nowDate) {
+ nowDate = nowDate ? toDate(nowDate) : new Date();
+ return (nowDate - toDate(date)) / 1000;
+ }
+ /**
+ * nextInterval: calculate the next interval time.
+ * - diff: the diff sec between now and date to be formated.
+ *
+ * What's the meaning?
+ * diff = 61 then return 59
+ * diff = 3601 (an hour + 1 second), then return 3599
+ * make the interval with high performace.
+ **/
+ function nextInterval(diff) {
+ var rst = 1, i = 0, d = Math.abs(diff);
+ for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+ diff /= SEC_ARRAY[i];
+ rst *= SEC_ARRAY[i];
+ }
+ // return leftSec(d, rst);
+ d = d % rst;
+ d = d ? rst - d : rst;
+ return Math.ceil(d);
+ }
+ // get the datetime attribute, jQuery and DOM
+ function getDateAttr(node) {
+ if (node.getAttribute) return node.getAttribute(ATTR_DATETIME);
+ if(node.attr) return node.attr(ATTR_DATETIME);
+ }
+ /**
+ * timeago: the function to get `timeago` instance.
+ * - nowDate: the relative date, default is new Date().
+ * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
+ *
+ * How to use it?
+ * var timeagoLib = require('timeago.js');
+ * var timeago = timeagoLib(); // all use default.
+ * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
+ * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
+ * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
+ **/
+ function Timeago(nowDate, defaultLocale) {
+ var timers = {}; // real-time render timers
+ // if do not set the defaultLocale, set it with `en`
+ if (! defaultLocale) defaultLocale = 'en'; // use default build-in locale
+ // what the timer will do
+ function doRender(node, date, locale, cnt) {
+ var diff = diffSec(date, nowDate);
+ node.innerHTML = formatDiff(diff, locale, defaultLocale);
+ // waiting %s seconds, do the next render
+ timers['k' + cnt] = setTimeout(function() {
+ doRender(node, date, locale, cnt);
+ }, nextInterval(diff) * 1000);
+ }
+ /**
+ * nextInterval: calculate the next interval time.
+ * - diff: the diff sec between now and date to be formated.
+ *
+ * What's the meaning?
+ * diff = 61 then return 59
+ * diff = 3601 (an hour + 1 second), then return 3599
+ * make the interval with high performace.
+ **/
+ // this.nextInterval = function(diff) { // for dev test
+ // var rst = 1, i = 0, d = Math.abs(diff);
+ // for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {
+ // diff /= SEC_ARRAY[i];
+ // rst *= SEC_ARRAY[i];
+ // }
+ // // return leftSec(d, rst);
+ // d = d % rst;
+ // d = d ? rst - d : rst;
+ // return Math.ceil(d);
+ // }; // for dev test
+ /**
+ * format: format the date to *** time ago, with setting or default locale
+ * - date: the date / string / timestamp to be formated
+ * - locale: the formated string's locale name, e.g. en / zh_CN
+ *
+ * How to use it?
+ * var timeago = require('timeago.js')();
+ * timeago.format(new Date(), 'pl'); // Date instance
+ * timeago.format('2016-09-10', 'fr'); // formated date string
+ * timeago.format(1473473400269); // timestamp with ms
+ **/
+ this.format = function(date, locale) {
+ return formatDiff(diffSec(date, nowDate), locale, defaultLocale);
+ };
+ /**
+ * render: render the DOM real-time.
+ * - nodes: which nodes will be rendered.
+ * - locale: the locale name used to format date.
+ *
+ * How to use it?
+ * var timeago = new require('timeago.js')();
+ * // 1. javascript selector
+ * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
+ * // 2. use jQuery selector
+ * timeago.render($('.need_to_be_rendered'), 'pl');
+ *
+ * Notice: please be sure the dom has attribute `datetime`.
+ **/
+ this.render = function(nodes, locale) {
+ if (nodes.length === undefined) nodes = [nodes];
+ for (var i = 0; i < nodes.length; i++) {
+ doRender(nodes[i], getDateAttr(nodes[i]), locale, ++ cnt); // render item
+ }
+ };
+ /**
+ * cancel: cancel all the timers which are doing real-time render.
+ *
+ * How to use it?
+ * var timeago = new require('timeago.js')();
+ * timeago.render(document.querySelectorAll('.need_to_be_rendered'));
+ * timeago.cancel(); // will stop all the timer, stop render in real time.
+ **/
+ this.cancel = function() {
+ for (var key in timers) {
+ clearTimeout(timers[key]);
+ }
+ timers = {};
+ };
+ /**
+ * setLocale: set the default locale name.
+ *
+ * How to use it?
+ * var timeago = require('timeago.js');
+ * timeago = new timeago();
+ * timeago.setLocale('fr');
+ **/
+ this.setLocale = function(locale) {
+ defaultLocale = locale;
+ };
+ return this;
+ }
+ /**
+ * timeago: the function to get `timeago` instance.
+ * - nowDate: the relative date, default is new Date().
+ * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.
+ *
+ * How to use it?
+ * var timeagoLib = require('timeago.js');
+ * var timeago = timeagoLib(); // all use default.
+ * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.
+ * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.
+ * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.
+ **/
+ function timeagoFactory(nowDate, defaultLocale) {
+ return new Timeago(nowDate, defaultLocale);
+ }
+ /**
+ * register: register a new language locale
+ * - locale: locale name, e.g. en / zh_CN, notice the standard.
+ * - localeFunc: the locale process function
+ *
+ * How to use it?
+ * var timeagoLib = require('timeago.js');
+ *
+ * timeagoLib.register('the locale name', the_locale_func);
+ * // or
+ * timeagoLib.register('pl', require('timeago.js/locales/pl'));
+ **/
+ timeagoFactory.register = function(locale, localeFunc) {
+ locales[locale] = localeFunc;
+ };
+
+ return timeagoFactory;
+}); \ No newline at end of file
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index 4fd1e3fc1d3..961859dfb5b 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 */
+/* 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 */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 44a66a915e3..6872186cd7f 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 */
+/* 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 */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index ea5a60bb78e..b0f834705c3 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, 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, max-len */
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index d4f86534f0c..9404b2c3a8c 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */
(function() {
Turbolinks.enableProgressBar();
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0bd90c57396..7741cd29793 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
(function() {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 6da3942ea52..9e4ffd07dbd 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
@@ -36,7 +36,7 @@
this.loadEditor();
}
},
- ready() {
+ mounted() {
if (this.file.loadEditor) {
this.loadEditor();
}
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
deleted file mode 100644
index 797850262cc..00000000000
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
+++ /dev/null
@@ -1,15 +0,0 @@
-/* eslint-disable */
-((global) => {
-
- global.mergeConflicts = global.mergeConflicts || {};
-
- global.mergeConflicts.parallelConflictLine = Vue.extend({
- props: {
- file: Object,
- line: Object
- },
- mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
- template: '#parallel-conflict-line'
- });
-
-})(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 1b3e9901f1e..4ccbdcd6daa 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
@@ -7,10 +7,22 @@
props: {
file: Object
},
- mixins: [global.mergeConflicts.utils],
- components: {
- 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
- }
+ mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
+ template: `
+ <table>
+ <tr class="line_holder parallel" v-for="section in file.parallelLines">
+ <template v-for="line in section">
+ <td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td>
+ <td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader">
+ <strong>{{line.richText}}</strong>
+ <button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button>
+ </td>
+ <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
+ <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
+ </template>
+ </tr>
+ </table>
+ `,
});
})(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 222a5dcfc2e..815443fb54e 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -6,7 +6,6 @@
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
@@ -49,7 +48,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
- $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
+ $('.js-syntax-highlight').syntaxHighlight();
});
});
},
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d3bd1e846c1..a4b4db14db8 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */
/*= require jquery.waitforimages */
/*= require task_list */
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 860ee5df57e..b1928f8d279 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable max-len, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-undef, one-var, one-var-declaration-per-line, quotes, comma-dangle, consistent-return, prefer-template, no-param-reassign, camelcase, vars-on-top, space-in-parens, curly, prefer-arrow-callback, no-unused-vars, no-return-assign, semi, object-shorthand, operator-assignment, padded-blocks, max-len */
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
@@ -130,7 +130,7 @@
MergeRequestTabs.prototype.scrollToElement = function(container) {
var $el, navBarHeight;
if (window.location.hash) {
- navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight;
$el = $(container + " " + window.location.hash + ":not(.match)");
if ($el.length) {
return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
@@ -145,7 +145,8 @@
if (action === 'show') {
action = 'notes';
}
- $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
+ // important note: the .tab('show') method triggers 'shown.bs.tab' event itself
+ $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
};
// Replaces the current Merge Request-specific action in the URL with a new one
@@ -227,8 +228,8 @@
return function(data) {
$('#diffs').html(data.html);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 3a2fe454b68..56c87af3226 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -218,7 +218,7 @@
}
if (environment.deployed_at && environment.deployed_at_formatted) {
- environment.deployed_at = $.timeago(environment.deployed_at) + '.';
+ environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.';
} else {
$('.js-environment-timeago', $template).remove();
environment.name += '.';
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 7ad86d8c084..15a12c3d985 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 9299c96e8ea..db7561a3a75 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, 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 */
(function() {
this.Milestone = (function() {
Milestone.updateIssue = function(li, issue_url, data) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index c909b53dc21..67796083790 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, no-param-reassign, no-shadow, padded-blocks, max-len */
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) {
@@ -162,7 +162,7 @@
if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace;
data.milestone.path = _this.currentProject.path;
- data.milestone.remaining = $.timefor(data.milestone.due_date);
+ data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index d1168227b77..87c903ec576 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 74dbeb94741..e3dc599b90a 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 8898e7ace43..5a8f723a27b 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
(function() {
this.Network = (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 42d6799c82f..732d92845cb 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */
// 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
@@ -9,6 +9,8 @@
(function() {
$(function() {
+ if (!$(".network-graph").length) return;
+
var network_graph;
network_graph = new Network({
url: $(".network-graph").attr('data-url'),
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 0e643b0ff14..29a323dd4c6 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(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; };
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index acb529023fa..8fb8f3e4a5f 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 4976eef2896..6cb87f9ba81 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable 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, no-undef, 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, max-len */
/*= require autosave */
/*= require autosize */
@@ -12,7 +12,7 @@
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.Notes = (function() {
- var isMetaKey;
+ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
Notes.interval = null;
@@ -33,6 +33,7 @@
this.resetMainTargetForm = bind(this.resetMainTargetForm, this);
this.refresh = bind(this.refresh, this);
this.keydownNoteText = bind(this.keydownNoteText, this);
+ this.toggleCommitList = bind(this.toggleCommitList, this);
this.notes_url = notes_url;
this.note_ids = note_ids;
this.last_fetched_at = last_fetched_at;
@@ -46,6 +47,7 @@
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.initTaskList();
+ this.collapseLongCommitList();
}
Notes.prototype.addBinding = function() {
@@ -81,10 +83,13 @@
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
// hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ // toggle commit list
+ $(document).on("click", '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
$(document).on("visibilitychange", this.visibilityChange);
// when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
+
// when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -114,9 +119,10 @@
Notes.prototype.keydownNoteText = function(e) {
var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
- if (isMetaKey(e)) {
+ if (gl.utils.isMetaKey(e)) {
return;
}
+
$textarea = $(e.target);
// Edit previous note when UP arrow is hit
switch (e.which) {
@@ -156,10 +162,6 @@
}
};
- isMetaKey = function(e) {
- return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
- };
-
Notes.prototype.initRefresh = function() {
clearInterval(Notes.interval);
return Notes.interval = setInterval((function(_this) {
@@ -263,6 +265,7 @@
$notesList.append(note.html).syntaxHighlight();
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
+ this.collapseLongCommitList();
this.initTaskList();
this.refresh();
return this.updateNotesCount(1);
@@ -325,8 +328,8 @@
discussionContainer.append(note_html);
}
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', note_html), false);
@@ -433,9 +436,9 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var projectPath = $form.data('project-path')
- discussionId = $form.data('discussion-id'),
- mergeRequestId = $form.data('noteable-iid');
+ var projectPath = $form.data('project-path');
+ var discussionId = $form.data('discussion-id');
+ var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
@@ -466,8 +469,8 @@
$note_li.replaceWith($html);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
};
@@ -559,11 +562,9 @@
note = $(el);
notes = note.closest(".notes");
- if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
- ref = DiffNotesApp.$refs[noteId];
-
- if (ref) {
- ref.$destroy(true);
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (gl.diffNoteApps[noteId]) {
+ gl.diffNoteApps[noteId].$destroy();
}
}
@@ -643,11 +644,12 @@
form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
- if (typeof DiffNotesApp !== 'undefined') {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
- DiffNotesApp.$compile($commentBtn.get(0));
+
+ gl.diffNotesCompileComponents();
}
form.find(".js-note-text").focus();
@@ -845,9 +847,9 @@
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
};
- Notes.prototype.resolveDiscussion = function () {
- var $this = $(this),
- discussionId = $this.attr('data-discussion-id');
+ Notes.prototype.resolveDiscussion = function() {
+ var $this = $(this);
+ var discussionId = $this.attr('data-discussion-id');
$this
.closest('form')
@@ -856,6 +858,36 @@
.attr('data-project-path', $this.attr('data-project-path'));
};
+ Notes.prototype.toggleCommitList = function(e) {
+ const $element = $(e.target);
+ const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
+
+ $closestSystemCommitList.toggleClass('hide-shade');
+ };
+
+ /**
+ Scans system notes with `ul` elements in system note body
+ then collapse long commit list pushed by user to make it less
+ intrusive.
+ */
+ Notes.prototype.collapseLongCommitList = function() {
+ const systemNotes = $('#notes-list').find('li.system-note').has('ul');
+
+ $.each(systemNotes, function(index, systemNote) {
+ const $systemNote = $(systemNote);
+ const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', '');
+
+ $systemNote.find('.note-header .system-note-message').html(headerMessage);
+
+ if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) {
+ $systemNote.find('.note-text').addClass('system-note-commit-list');
+ $systemNote.find('.system-note-commit-list-toggler').show();
+ } else {
+ $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
+ }
+ });
+ };
+
return Notes;
})();
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index ef3f2c6ae73..b152d26733f 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, padded-blocks, max-len */
(function() {
this.NotificationsDropdown = (function() {
function NotificationsDropdown() {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 6fbec8efe9b..2034f9a748a 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 2e4dc62273e..d22d2d9dbae 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index e6fada5c84c..a84db9c0233 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -3,26 +3,12 @@
class Pipelines {
constructor() {
- this.initGraphToggle();
this.addMarginToBuildColumns();
}
- initGraphToggle() {
- this.pipelineGraph = document.querySelector('.pipeline-graph');
- this.toggleButton = document.querySelector('.toggle-pipeline-btn');
- this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
- this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
- }
-
- toggleGraph() {
- const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
- this.toggleButton.classList.toggle('graph-collapsed');
- this.pipelineGraph.classList.toggle('graph-collapsed');
- this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
- }
-
addMarginToBuildColumns() {
- const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+ this.pipelineGraph = document.querySelector('.pipeline-graph');
+ const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
for (buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index f2a45a18bed..3723aa24942 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 2d0c6b16699..016d999d77e 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, 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 */
(function() {
this.Project = (function() {
function Project() {
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 61877c6616d..84f28ede4bf 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index ddac5ed83e1..804306a3293 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index fd95f8f2c19..4aedc9a2330 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index f1c4a9fe542..c99e55234cf 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
new file mode 100644
index 00000000000..03a115cb35b
--- /dev/null
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -0,0 +1,53 @@
+/* eslint-disable */
+(function(global) {
+ class ProjectLabelSubscription {
+ constructor(container) {
+ this.$container = $(container);
+ this.$buttons = this.$container.find('.js-subscribe-button');
+
+ this.$buttons.on('click', this.toggleSubscription.bind(this));
+ }
+
+ toggleSubscription(event) {
+ event.preventDefault();
+
+ const $btn = $(event.currentTarget);
+ const $span = $btn.find('span');
+ const url = $btn.attr('data-url');
+ const oldStatus = $btn.attr('data-status');
+
+ $btn.addClass('disabled');
+ $span.toggleClass('hidden');
+
+ $.ajax({
+ type: 'POST',
+ url: url
+ }).done(() => {
+ let newStatus, newAction;
+
+ if (oldStatus === 'unsubscribed') {
+ [newStatus, newAction] = ['subscribed', 'Unsubscribe'];
+ } else {
+ [newStatus, newAction] = ['unsubscribed', 'Subscribe'];
+ }
+
+ $span.toggleClass('hidden');
+ $btn.removeClass('disabled');
+
+ this.$buttons.attr('data-status', newStatus);
+ this.$buttons.find('> span').text(newAction);
+
+ for (let button of this.$buttons) {
+ let $button = $(button);
+
+ if ($button.attr('data-original-title')) {
+ $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
+ }
+ }
+ });
+ }
+ }
+
+ global.ProjectLabelSubscription = ProjectLabelSubscription;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 0d3fb31a9cf..7fc611d0dad 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index e1acf3c8232..fe1f96872f3 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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-undef, no-else-return, quotes, padded-blocks, max-len */
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 21650f5f67a..eaf4c03d573 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 3458cd89ae2..dbf530bed41 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
(function() {
this.ProjectsList = {
init: function() {
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index df38937858f..440b5da756d 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d79e6f014f6..1d208f1494c 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
(function() {
this.Search = (function() {
function Search() {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 8d8ab6dda5e..fa2168723be 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index 704a8bd3a57..65305b8c22f 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index befe4eccdba..1b9a265ba39 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 90ed4267661..68cd6fad04e 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 25ec7dbc067..c4899f3566a 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, padded-blocks, max-len */
/*= require mousetrap */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 19c6b7d30ab..7d4d6364c70 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 002e979a2c6..a4095d2c06b 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 8e54ca4f0dc..2767849e673 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -45,15 +45,15 @@
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
@@ -76,8 +76,8 @@
}
_this.collapsedContent.after(_this.content);
- if (typeof DiffNotesApp !== 'undefined') {
- DiffNotesApp.compileComponents();
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ gl.diffNotesCompileComponents();
}
if (cb) cb();
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 083dc23c796..2c8ecba7de4 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index cfd1e2204d5..32803fa790b 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, padded-blocks, max-len */
(function() {
this.Star = (function() {
function Star() {
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index f9915593657..6d75688deeb 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, one-var, one-var-declaration-per-line, camelcase, consistent-return, no-undef, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index 2ca65cb762d..185d20775d0 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 77ad4f30b7a..bd37d69165f 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 70aff4b9a2f..54c473d936d 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
this.TreeView = (function() {
function TreeView() {
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 35f2b1e2b25..5d991542b51 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index aff605169e4..4c70a6e9bb6 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 22fbf9f3a91..97d8993cac2 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 2eab2d5ae23..eedd3bcd5a1 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 0ec878e7e60..6d739039a5b 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index 22bee0f6187..f50802bdf2e 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren */
/*= require_tree . */
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 7a2221dbaf5..c6e18fad832 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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, no-undef, 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, max-len */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice;
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6
new file mode 100644
index 00000000000..fd628fad4d7
--- /dev/null
+++ b/app/assets/javascripts/vue_common_component/commit.js.es6
@@ -0,0 +1,176 @@
+/*= require vue */
+/* global Vue */
+(() => {
+ window.gl = window.gl || {};
+
+ window.gl.CommitComponent = Vue.component('commit-component', {
+
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ ref: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+
+ /**
+ * Used to link to the commit sha.
+ */
+ commit_url: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * Used to show the commit short_sha that links to the commit url.
+ */
+ short_sha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasRef() {
+ return this.ref && this.ref.name && this.ref.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ /**
+ * In order to reuse the svg instead of copy and paste in this template
+ * we need to render it outside this component using =custom_icon partial.
+ * Make sure it has this structure:
+ * .commit-icon-svg.hidden
+ * svg
+ *
+ * TODO: Find a better way to include SVG
+ */
+ mounted() {
+ const commitIconContainer = this.$el.querySelector('.commit-icon-container');
+ const commitIcon = document.querySelector('.commit-icon-svg.hidden svg');
+
+ if (commitIconContainer && commitIcon) {
+ commitIconContainer.appendChild(commitIcon.cloneNode(true));
+ }
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
+ </div>
+
+ <a v-if="hasRef"
+ class="monospace branch-name"
+ :href="ref.ref_url"
+ v-html="ref.name">
+ </a>
+
+ <div class="icon-container commit-icon commit-icon-container">
+ </div>
+
+ <a class="commit-id monospace"
+ :href="commit_url"
+ v-html="short_sha">
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commit_url" v-html="title">
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index ad9b842db3c..5dd853389c2 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, consistent-return, one-var, one-var-declaration-per-line, no-undef, prefer-template, padded-blocks, max-len */
/*= require latinise */
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index fa124e7052d..82eb761442a 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index d5cca1b10fb..7c7f991dd87 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -39,3 +39,5 @@
@import "framework/typography.scss";
@import "framework/zen.scss";
@import "framework/blank";
+@import "framework/wells.scss";
+@import "framework/page-header.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 202ed5ae8fe..ad0d387067f 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -34,6 +34,7 @@
&.avatar-inline {
float: none;
+ display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
@@ -41,6 +42,12 @@
&.s24 { margin-right: 4px; }
}
+ &.center {
+ font-size: 14px;
+ line-height: 1.8em;
+ text-align: center;
+ }
+
&.avatar-tile {
border-radius: 0;
border: none;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ed21ad83a1c..4a9aa0f8717 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -6,7 +6,6 @@
&:focus,
&:active {
- outline: none;
background-color: $btn-active-gray;
box-shadow: $gl-btn-active-background;
}
@@ -142,6 +141,10 @@
&.btn-save {
@include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
}
+
+ &.btn-remove {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
}
&.btn-gray {
@@ -267,10 +270,6 @@
outline: none;
}
- &:focus {
- outline: none;
- }
-
&:active {
outline: none;
}
@@ -350,6 +349,12 @@
}
}
+.btn-inverted {
+ &-secondary {
+ @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
+ }
+}
+
@media (max-width: $screen-xs-max) {
.btn-wide-on-xs {
width: 100%;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index ad5ac589d0f..7f5583c917a 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -376,3 +376,19 @@ table {
}
.hide-bottom-border { border-bottom: none !important; }
+
+.gl-accessibility {
+ &:focus {
+ top: 1px;
+ left: 1px;
+ width: auto;
+ height: 100%;
+ line-height: 50px;
+ padding: 0 10px;
+ clip: auto;
+ text-decoration: none;
+ color: $gl-title-color;
+ background: $gray-light;
+ z-index: 1;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3e34ec98427..583c17e4a83 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -38,7 +38,6 @@
text-align: left;
border: 1px solid $border-color;
border-radius: $border-radius-base;
- outline: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@@ -55,6 +54,10 @@
}
}
+ &.no-outline {
+ outline: 0;
+ }
+
&:hover, {
border-color: $dropdown-toggle-hover-border-color;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4993ca7572a..16ecf466931 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -63,6 +63,11 @@ header {
&:focus,
&:active {
background-color: $background-color;
+ color: darken($gl-icon-color, 30%);
+
+ .todos-pending-count {
+ background: darken($todo-alert-blue, 10%);
+ }
}
.fa-caret-down {
@@ -100,10 +105,6 @@ header {
&:hover {
background-color: $btn-gray-hover;
}
-
- &:focus {
- outline: none;
- }
}
}
@@ -156,7 +157,7 @@ header {
padding-right: 20px;
margin: 0;
font-size: 19px;
- max-width: 400px;
+ max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -195,6 +196,10 @@ header {
font-size: 10px;
text-align: center;
cursor: pointer;
+
+ &:hover {
+ color: darken($color: $gl-text-color, $amount: 30%);
+ }
}
.project-item-select {
@@ -222,6 +227,14 @@ header {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-lg-min) {
+ .header-content .title {
+ width: 300px;
+ }
+ }
+}
+
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 07c8874bf03..909a0f4afda 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -11,7 +11,7 @@
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
- line-height: $code_line_height !important;
+ line-height: 19px;
margin: 0;
overflow: auto;
overflow-y: hidden;
@@ -47,7 +47,7 @@
font-family: $monospace_font;
display: block;
font-size: $code_font_size !important;
- line-height: $code_line_height !important;
+ line-height: 19px;
white-space: nowrap;
i {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 6d28d98b283..42087c91530 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -64,12 +64,17 @@
a {
padding-top: 0;
- line-height: 1;
+ line-height: 19px;
border-bottom: 1px solid $border-color;
&.btn.btn-xs {
padding: 2px 5px;
}
+
+ &:focus {
+ margin-top: -10px;
+ padding-top: 10px;
+ }
}
}
}
@@ -148,7 +153,19 @@
}
}
-.atwho-view small.description {
- float: right;
- padding: 3px 5px;
+.atwho-view {
+ small.description {
+ float: right;
+ padding: 3px 5px;
+ }
+
+ .avatar-inline {
+ margin-bottom: 0;
+ }
+
+ .cur {
+ .avatar {
+ border: 1px solid $white-light;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c1ed43bc20f..9391661a595 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -71,7 +71,7 @@
display: none;
}
- .group-right-buttons {
+ .group-buttons {
display: none;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index fcaf5e18633..ce864c2de5e 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -58,7 +58,6 @@
&:active,
&:focus {
text-decoration: none;
- outline: none;
}
}
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
new file mode 100644
index 00000000000..85c1385d5d9
--- /dev/null
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -0,0 +1,67 @@
+.page-content-header {
+ line-height: 34px;
+ padding: 10px 0;
+ margin-bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ display: flex;
+ align-items: center;
+
+ .header-main-content {
+ flex: 1;
+ }
+ }
+
+ .header-action-buttons {
+ i {
+ color: $gl-icon-color;
+ font-size: 13px;
+ margin-right: 3px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ width: 100%;
+ margin-top: 10px;
+ }
+
+ .dropdown {
+ width: 100%;
+ }
+ }
+ }
+
+ .avatar {
+ @extend .avatar-inline;
+ margin-left: 0;
+
+ @media (min-width: $screen-sm-min) {
+ margin-left: 4px;
+ }
+ }
+
+ .commit-committer-link,
+ .commit-author-link {
+ color: $gl-gray;
+ font-weight: bold;
+ }
+
+ .fa-clipboard {
+ color: $dropdown-title-btn-color;
+ }
+
+ .commit-info {
+ &.branches {
+ margin-left: 8px;
+ }
+ }
+
+ .ci-status-link {
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 13749f1b7bd..920ce249b9a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -63,7 +63,7 @@
}
.select2-highlighted {
- background: #3084bb !important;
+ background: $gl-link-color !important;
}
.select2-results li.select2-result-with-children > .select2-result-label {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d74c14ee2a4..44c445c0543 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -83,7 +83,6 @@
display: block;
text-decoration: none;
font-weight: normal;
- outline: none;
&:hover,
&:active,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index be2a7ceefff..92226f7432e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -90,8 +90,8 @@ $table-border-color: #f0f0f0;
$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
-$widget-expand-item: #e8f2f7;
-$widget-inner-border: #eef0f2;
+$well-expand-item: #e8f2f7;
+$well-inner-border: #eef0f2;
/*
* Text
@@ -103,7 +103,7 @@ $gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
-$gl-link-color: #3084bb;
+$gl-link-color: #3777b0;
$gl-dark-link-color: #333;
$gl-placeholder-color: #8f8f8f;
$gl-icon-color: $gl-placeholder-color;
@@ -197,7 +197,7 @@ $line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
$match-line: $gray-light;
$table-border-gray: #f0f0f0;
-$line-target-blue: #eaf3fc;
+$line-target-blue: #f6faff;
$line-select-yellow: #fcf8e7;
$line-select-yellow-dark: #f0e2bd;
@@ -252,7 +252,7 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
*/
$search-input-border-color: rgba(#4688f1, .8);
$search-input-focus-shadow-color: $dropdown-input-focus-shadow;
-$search-input-width: 244px;
+$search-input-width: 220px;
$location-badge-color: #aaa;
$location-badge-bg: $gray-normal;
$location-badge-active-bg: #4f91f8;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
new file mode 100644
index 00000000000..192939f4527
--- /dev/null
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -0,0 +1,45 @@
+.info-well {
+ background: $background-color;
+ color: $gl-gray;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ .well-segment {
+ padding: $gl-padding;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $well-inner-border;
+ }
+
+ &.branch-info {
+ .monospace,
+ .commit-info {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ margin-right: 8px;
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &.commit-icon {
+ svg {
+ path {
+ fill: $gl-text-color;
+ }
+ }
+ }
+ }
+
+ .label.label-gray {
+ background-color: $well-expand-item;
+ }
+}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 6cefafd8fc7..14812e171fd 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -160,3 +160,9 @@
}
}
}
+
+.admin-builds-table {
+ .ci-table td:last-child {
+ min-width: 120px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 47a7e84b5c6..4f5753f6fc6 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -166,8 +166,12 @@
}
}
-.board-list {
+.board-list-component {
height: calc(100% - 49px);
+}
+
+.board-list {
+ height: 100%;
margin-bottom: 0;
padding: 5px;
list-style: none;
@@ -175,7 +179,7 @@
overflow-x: hidden;
&.is-smaller {
- height: calc(100% - 185px);
+ height: calc(100% - 136px);
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6300ac9662f..48f11eb2552 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -14,18 +14,10 @@
}
}
- .autoscroll-container {
- position: fixed;
- bottom: 20px;
- right: 20px;
- z-index: 100;
- }
-
.scroll-controls {
- &.affix-top {
- position: absolute;
- top: 10px;
- right: 25px;
+ .scroll-step {
+ width: 31px;
+ margin: 0 0 0 auto;
}
&.affix-bottom {
@@ -34,13 +26,13 @@
}
&.affix {
- right: 30px;
+ right: 25px;
bottom: 15px;
z-index: 1;
+ }
- @media (min-width: $screen-md-min) {
- right: 26%;
- }
+ &.sidebar-expanded {
+ right: #{$gutter_width + ($gl-padding * 2)};
}
a {
@@ -48,6 +40,19 @@
margin-bottom: 10px;
}
}
+
+ .environment-information {
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ padding: 12px $gl-padding;
+ border-radius: $border-radius-default;
+
+ svg {
+ position: relative;
+ top: 1px;
+ margin-right: 5px;
+ }
+ }
}
.build-header {
@@ -57,10 +62,6 @@
min-height: 58px;
align-items: center;
- .btn-inverted {
- @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
- }
-
@media (max-width: $screen-sm-max) {
padding-right: 40px;
@@ -71,14 +72,14 @@
.header-content {
flex: 1;
- }
- a {
- color: $gl-gray;
+ a {
+ color: $gl-gray;
- &:hover {
- color: $gl-link-color;
- text-decoration: none;
+ &:hover {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 47d3e72679b..ddc9d0e2b1a 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,143 +26,12 @@
white-space: pre-wrap;
}
-.commit-info-row {
- margin-bottom: 10px;
- line-height: 24px;
- padding-top: 6px;
-
- &.commit-info-row-header {
- line-height: 34px;
- padding: 10px 0;
- margin-bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- display: flex;
- align-items: center;
-
- .commit-meta {
- flex: 1;
- }
- }
-
- .commit-hash-full {
- @media (max-width: $screen-sm-max) {
- width: 80px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
-
- .commit-action-buttons {
- i {
- color: $gl-icon-color;
- font-size: 13px;
- margin-right: 3px;
- }
-
- @media (max-width: $screen-xs-max) {
- .dropdown {
- width: 100%;
- margin-top: 10px;
- }
-
- .dropdown-toggle {
- width: 100%;
- }
- }
- }
- }
-
- .avatar {
- @extend .avatar-inline;
- margin-left: 0;
-
- @media (min-width: $screen-sm-min) {
- margin-left: 4px;
- }
- }
-
- .commit-committer-link,
- .commit-author-link {
- color: $gl-gray;
- font-weight: bold;
- }
-
- .fa-clipboard {
- color: $dropdown-title-btn-color;
- }
-
- .commit-info {
- &.branches {
- margin-left: 8px;
- }
- }
-
- .ci-status-link {
-
- svg {
- position: relative;
- top: 2px;
- margin: 0 2px 0 3px;
- }
- }
-}
-
.js-details-expand {
&:hover {
text-decoration: none;
}
}
-.commit-info-widget {
- background: $background-color;
- color: $gl-gray;
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
-
- .widget-row {
- padding: $gl-padding;
-
- &:not(:last-of-type) {
- border-bottom: 1px solid $widget-inner-border;
- }
-
- &.branch-info {
- .monospace,
- .commit-info {
- margin-left: 4px;
- }
- }
- }
-
- .icon-container {
- display: inline-block;
- margin-right: 8px;
-
- svg {
- position: relative;
- top: 2px;
- height: 16px;
- width: 16px;
- }
-
- &.commit-icon {
- svg {
- path {
- fill: $gl-text-color;
- }
- }
- }
- }
-
- .label.label-gray {
- background-color: $widget-expand-item;
- }
-}
-
.ci-status-link {
svg {
overflow: visible;
@@ -184,6 +53,17 @@
}
}
+.commit-hash-full {
+ @media (max-width: $screen-sm-max) {
+ width: 80px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
+
.file-stats {
ul {
list-style: none;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 98a84351a3d..83ffa0e1d39 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -62,6 +62,8 @@
.ci-status-link {
display: inline-block;
+ position: relative;
+ top: 1px;
}
.btn-clipboard,
@@ -82,7 +84,8 @@
font-weight: 600;
}
-.commit {
+.commit,
+.generic_commit_status {
padding: 10px 0;
position: relative;
@@ -100,7 +103,6 @@
vertical-align: baseline;
}
-
.avatar {
margin-left: -46px;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index fde138c874d..99fdea15218 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -92,20 +92,6 @@
&.noteable_line {
position: relative;
-
- &.old {
- &::before {
- content: '-';
- position: absolute;
- }
- }
-
- &.new {
- &::before {
- content: '+';
- position: absolute;
- }
- }
}
span {
@@ -151,8 +137,9 @@
.line_content {
display: block;
margin: 0;
- padding: 0 0.5em;
+ padding: 0 1.5em;
border: none;
+ position: relative;
&.parallel {
display: table-cell;
@@ -161,6 +148,22 @@
word-break: break-all;
}
}
+
+ &.old {
+ &::before {
+ content: '-';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
+
+ &.new {
+ &::before {
+ content: '+';
+ position: absolute;
+ left: 0.5em;
+ }
+ }
}
.text-file.diff-wrap-lines table .line_holder td span {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index fc49ff780fc..e9ff43a8adb 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,10 +1,23 @@
-.environments-container,
.deployments-container {
width: 100%;
overflow: auto;
}
+.environments-list-loading {
+ width: 100%;
+ font-size: 34px;
+}
+
+@media (max-width: $screen-sm-min) {
+ .environments-container {
+ width: 100%;
+ overflow: auto;
+ }
+}
+
.environments {
+ table-layout: fixed;
+
.deployment-column {
.avatar {
float: none;
@@ -15,6 +28,10 @@
margin: 0;
}
+ .avatar-image-container {
+ text-decoration: none;
+ }
+
.icon-play {
height: 13px;
width: 12px;
@@ -38,7 +55,8 @@
color: $gl-dark-link-color;
}
- .stop-env-link {
+ .stop-env-link,
+ .external-url {
color: $table-text-gray;
.stop-env-icon {
@@ -58,10 +76,29 @@
}
}
}
+
+ .children-row .environment-name {
+ margin-left: 17px;
+ margin-right: -17px;
+ }
+
+ .folder-icon {
+ padding: 0 5px 0 0;
+ }
+
+ .folder-name {
+ cursor: pointer;
+
+ .badge {
+ font-weight: normal;
+ background-color: $gray-darker;
+ color: $gl-placeholder-color;
+ vertical-align: baseline;
+ }
+ }
}
.table.ci-table.environments {
-
.icon-container {
width: 20px;
text-align: center;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 4375e29c8db..57d028cec8c 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -10,7 +10,6 @@
}
.group-row {
-
.stats {
float: right;
line-height: $list-text-height;
@@ -23,36 +22,18 @@
}
.ldap-group-links {
-
.form-actions {
margin-bottom: $gl-padding;
}
}
-.groups-cover-block {
-
- .container-fluid {
- position: relative;
- }
-
- .group-right-buttons {
- position: absolute;
- right: 16px;
-
- .btn {
- @include btn-gray;
- padding: 3px 10px;
- background-color: $background-color;
- }
- }
-
- .group-avatar {
- border: 0;
+.group-buttons {
+ .notification-dropdown {
+ display: inline-block;
}
}
.groups-header {
-
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 230b927a17d..773155fe80a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -267,20 +267,6 @@
}
}
- .issuable-header-btn {
- background: $gray-normal;
- border: 1px solid $border-gray-normal;
-
- &:hover {
- background: $gray-dark;
- border: 1px solid $border-gray-dark;
- }
-
- &.btn-primary {
- @extend .btn-primary;
- }
- }
-
a {
&:hover {
color: $md-link-color;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3e7fc3fa52c..eb171195309 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -55,9 +55,28 @@ ul.related-merge-requests > li {
}
.merge-request-status {
- color: $gl-gray;
- font-size: 15px;
- font-weight: bold;
+ font-size: 13px;
+ padding: 0 5px;
+ color: $white-light;
+ height: 20px;
+ border-radius: 3px;
+ line-height: 18px;
+ border: 1px solid;
+
+ &.merged {
+ border-color: darken($blue-normal, 10%);
+ background: $blue-normal;
+ }
+
+ &.closed {
+ border-color: darken($red-normal, 10%);
+ background: $red-normal;
+ }
+
+ &.open {
+ border: 1px solid darken($green-normal, 10%);
+ background: $green-normal;
+ }
}
.merge-request,
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 397f89f501a..e39ce19f846 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -90,7 +90,7 @@
@media (min-width: $screen-sm-min) {
display: inline-block;
- width: 40%;
+ width: 30%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
@@ -222,6 +222,14 @@
width: 100%;
}
+.label-subscription {
+ vertical-align: middle;
+
+ .dropdown-group-label a {
+ cursor: pointer;
+ }
+}
+
.label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 10f67b47998..54c89d75e94 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -255,26 +255,3 @@
}
}
-// For sign in pane only, to improve tab order, the following removes the submit button from
-// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
-
-.login-box {
- .new_user {
- position: relative;
- padding-bottom: 35px;
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- .forgot-password {
- float: none !important;
- margin-top: 5px;
- }
- }
- }
-
- .move-submit-down {
- position: absolute;
- width: 100%;
- bottom: 0;
- }
-}
-
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 032feae8854..19ab198c2e7 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -228,7 +228,6 @@ $colors: (
position: absolute;
right: 10px;
padding: 0;
- outline: none;
color: #fff;
width: 75px; // static width to make 2 buttons have same width
height: 19px;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f8e31a624ec..b6a82460f25 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -47,6 +47,7 @@
&.right {
float: right;
+ padding-right: 0;
a {
color: $gl-gray;
@@ -60,7 +61,7 @@
}
.ci_widget {
- border-bottom: 1px solid $widget-inner-border;
+ border-bottom: 1px solid $well-inner-border;
svg {
margin-right: 4px;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 526e9ae5cdd..0dfd4ab7ec9 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -35,11 +35,84 @@ ul.notes {
.system-note {
font-size: 14px;
- padding-top: 10px;
- padding-bottom: 10px;
- background: #fdfdfd;
+ padding: 0;
+ clear: both;
+
+ &.timeline-entry::after {
+ clear: none;
+ }
+
+ .system-note-message {
+ text-transform: lowercase;
+
+ a {
+ color: $gl-link-color;
+ text-decoration: none;
+ }
+ }
+
+ .timeline-content {
+ padding: 14px 10px;
+ }
+
+ .note-body {
+ overflow: hidden;
+
+ .system-note-commit-list-toggler {
+ display: none;
+ padding: 10px 0 0;
+ cursor: pointer;
+ }
+
+ .note-text {
+ & p:first-child {
+ display: none;
+ }
+
+ &.system-note-commit-list {
+ max-height: 63px;
+ overflow: hidden;
+ display: block;
+
+ ul {
+ margin: 3px 0 3px 15px !important;
+
+ li {
+ font-family: $monospace_font;
+ font-size: 12px;
+ }
+ }
+
+ p:first-child {
+ display: none;
+ }
+
+ &::after {
+ content: '';
+ width: 100%;
+ height: 20px;
+ position: absolute;
+ left: 0;
+ bottom: 50px;
+ background: linear-gradient(rgba($gray-light, .3) 0, $white-light);
+ }
+
+ &.hide-shade {
+ max-height: 100%;
+ overflow: auto;
+
+ &::after {
+ display: none;
+ background: transparent;
+ }
+ }
+ }
+ }
+ }
.timeline-icon {
+ display: none;
+
.avatar {
visibility: hidden;
@@ -65,6 +138,12 @@ ul.notes {
position: relative;
border-bottom: 1px solid $table-border-gray;
+ &.note-discussion {
+ &.timeline-entry {
+ padding: 14px 10px;
+ }
+ }
+
&.is-editting {
.note-header,
.note-text,
@@ -88,10 +167,8 @@ ul.notes {
overflow: auto;
word-wrap: break-word;
@include md-typography;
-
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -111,6 +188,11 @@ ul.notes {
padding-bottom: 3px;
padding-right: 20px;
+ p {
+ display: inline;
+ margin: 0;
+ }
+
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@@ -141,6 +223,22 @@ ul.notes {
}
}
+.page-sidebar-pinned.right-sidebar-expanded {
+ @media (max-width: $screen-lg-min) {
+ .note-header {
+ .note-headline-light {
+ display: block;
+ }
+
+ .note-actions {
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+ }
+ }
+}
+
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
@@ -222,6 +320,10 @@ ul.notes {
}
}
+.discussion-header {
+ font-size: 14px;
+}
+
.note-headline-light,
.discussion-headline-light {
color: $notes-light-color;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index bf3cb6e7ad9..a44a496c728 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -109,10 +109,6 @@
float: none;
}
- .api {
- color: $code-color;
- }
-
.branch-commit {
.branch-name {
@@ -304,6 +300,8 @@
.pipeline-graph {
width: 100%;
+ background-color: $background-color;
+ padding: $gl-padding;
overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
@@ -367,6 +365,7 @@
.build {
border: 1px solid $border-color;
+ background-color: $white-light;
position: relative;
padding: 7px 10px 8px;
border-radius: 30px;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index ede29db1979..6fab97a71aa 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -23,6 +23,10 @@
color: $md-link-color;
}
+.private-tokens-reset div.reset-action:not(:first-child) {
+ padding-top: 15px;
+}
+
.oauth-buttons {
.btn-group {
margin-right: 10px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f7d54564530..19a7a97ea0d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -86,7 +86,8 @@
}
}
-.project-home-panel {
+.project-home-panel,
+.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
@@ -94,7 +95,8 @@
border-bottom: 1px solid $border-color;
}
- .project-avatar {
+ .project-avatar,
+ .group-avatar {
float: none;
margin: 0 auto;
border: none;
@@ -104,7 +106,8 @@
}
}
- .project-title {
+ .project-title,
+ .group-title {
margin-top: 10px;
margin-bottom: 10px;
font-size: 24px;
@@ -118,10 +121,11 @@
}
}
- .project-home-desc {
+ .project-home-desc,
+ .group-home-desc {
margin-left: auto;
margin-right: auto;
- margin-bottom: 15px;
+ margin-bottom: 0;
max-width: 700px;
> p {
@@ -141,13 +145,22 @@
}
}
-.project-repo-buttons {
- font-size: 0;
+.nav > .project-repo-buttons {
+ margin-top: 0;
+}
+
+.project-repo-buttons,
+.group-buttons {
+ margin-top: 15px;
.btn {
@include btn-gray;
padding: 3px 10px;
+ &:last-child {
+ margin-left: 0;
+ }
+
.fa {
color: $layout-link-gray;
}
@@ -168,12 +181,19 @@
}
}
- .project-repo-btn-group,
+ .download-button,
+ .dropdown-toggle,
.notification-dropdown,
.project-dropdown {
margin-left: 10px;
}
+ .download-button {
+ @media (max-width: $screen-lg-min) {
+ margin-left: 0;
+ }
+ }
+
.count-buttons {
display: inline-block;
vertical-align: top;
@@ -458,6 +478,20 @@ a.deploy-project-label {
}
}
+.page-sidebar-pinned {
+ .project-stats .nav > li.right {
+ @media (min-width: $screen-lg-min) {
+ float: none;
+ }
+ }
+
+ .download-button {
+ @media (min-width: $screen-lg-min) {
+ margin-left: 0;
+ }
+ }
+}
+
.project-stats {
font-size: 0;
border-bottom: 1px solid $border-color;
@@ -474,12 +508,12 @@ a.deploy-project-label {
margin-right: $gl-padding;
}
- &.project-repo-buttons-right {
- margin-top: 10px;
+ &.right {
+ vertical-align: top;
+ margin-top: 0;
- @media (min-width: $screen-md-min) {
+ @media (min-width: $screen-lg-min) {
float: right;
- margin-top: 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index bf688af50e2..63d0a34e610 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -8,6 +8,10 @@
border-bottom: none;
}
}
+
+ .blob-result {
+ margin: 5px 0;
+ }
}
.search {
@@ -21,6 +25,11 @@
padding: 4px;
width: $search-input-width;
line-height: 24px;
+
+ &:hover {
+ border-color: lighten($dropdown-input-focus-border, 20%);
+ box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
+ }
}
.location-text {
@@ -28,11 +37,9 @@
}
.search-input {
- padding-right: 20px;
border: none;
font-size: 14px;
- outline: none;
- padding: 0;
+ padding: 0 20px 0 0;
margin-left: 5px;
line-height: 25px;
width: 98%;
@@ -229,6 +236,5 @@
&:hover,
&:focus {
color: $gl-link-color;
- outline: none;
}
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 52e0256943a..b81842e319b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -117,6 +117,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:enabled_git_access_protocol,
+ :sidekiq_throttling_enabled,
+ :sidekiq_throttling_factor,
:housekeeping_enabled,
:housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period,
@@ -125,7 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
- disabled_oauth_sign_in_sources: []
+ disabled_oauth_sign_in_sources: [],
+ sidekiq_throttling_queues: []
)
end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index daa82336208..5c44637fdee 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -55,7 +55,13 @@ class AutocompleteController < ApplicationController
def find_users
@users =
if @project
- @project.team.users
+ user_ids = @project.team.users.pluck(:id)
+
+ if params[:author_id].present?
+ user_ids << params[:author_id]
+ end
+
+ User.where(id: user_ids)
elsif params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
new file mode 100644
index 00000000000..2aaf8f2b451
--- /dev/null
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -0,0 +1,7 @@
+module CycleAnalyticsParams
+ extend ActiveSupport::Concern
+
+ def start_date(params)
+ params[:start_date] == '30' ? 30.days.ago : 90.days.ago
+ end
+end
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
index aeec3009f15..1efa9fe060f 100644
--- a/app/controllers/concerns/diff_for_path.rb
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -3,7 +3,7 @@ module DiffForPath
def render_diff_for_path(diffs)
diff_file = diffs.diff_files.find do |diff|
- diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
+ diff.file_identifier == params[:file_identifier]
end
return render_404 unless diff_file
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index be86fa106f8..0821974aa93 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -12,7 +12,7 @@ module IssuableActions
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
TodoService.new.public_send(destroy_method, issuable, current_user)
- name = issuable.class.name.titleize.downcase
+ name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b5e79099e39..6247934f81e 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -10,11 +10,11 @@ module IssuableCollections
private
def issues_collection
- issues_finder.execute
+ issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
def merge_requests_collection
- merge_requests_finder.execute
+ merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :merge_request_diff, target_project: :namespace)
end
def issues_finder
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b89fb94be6e..b46adcceb60 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -7,7 +7,6 @@ module IssuesAction
@issues = issues_collection
.non_archived
- .preload(:author, :project)
.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index a1b0eee37f9..6546a07b41c 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -7,7 +7,6 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.non_archived
- .preload(:author, :target_project)
.page(params[:page])
end
end
diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb
index 9e3b9be2ff4..92cb534343e 100644
--- a/app/controllers/concerns/toggle_subscription_action.rb
+++ b/app/controllers/concerns/toggle_subscription_action.rb
@@ -4,13 +4,17 @@ module ToggleSubscriptionAction
def toggle_subscription
return unless current_user
- subscribable_resource.toggle_subscription(current_user)
+ subscribable_resource.toggle_subscription(current_user, subscribable_project)
head :ok
end
private
+ def subscribable_project
+ @project || raise(NotImplementedError)
+ end
+
def subscribable_resource
raise NotImplementedError
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 29528b2cfaa..587898a8634 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -1,4 +1,6 @@
class Groups::LabelsController < Groups::ApplicationController
+ include ToggleSubscriptionAction
+
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -69,6 +71,11 @@ class Groups::LabelsController < Groups::ApplicationController
def label
@label ||= @group.labels.find(params[:id])
end
+ alias_method :subscribable_resource, :label
+
+ def subscribable_project
+ nil
+ end
def label_params
params.require(:label).permit(:title, :description, :color)
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 9d5a28e8d4d..506484932cc 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params)
- @milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids
+ @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 4eca278599f..4b3c71874be 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -7,8 +7,8 @@ class HelpController < ApplicationController
@help_index = File.read(Rails.root.join('doc', 'README.md'))
# Prefix Markdown links with `help/` unless they already have been
- # See http://rubular.com/r/nwwhzH6Z8X
- @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3')
+ # See http://rubular.com/r/ie2MlpdUMq
+ @help_index.gsub!(/(\]\()(\/?help\/)?([^\)\(]+\))/, '\1/help/\3')
end
def show
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 7e4da73bc11..c736200a104 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -12,7 +12,7 @@ class JwtController < ApplicationController
return head :not_found unless service
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
- execute(authentication_abilities: @authentication_result.authentication_abilities || [])
+ execute(authentication_abilities: @authentication_result.authentication_abilities)
render json: result, status: result[:http_status]
end
@@ -20,7 +20,7 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
- @authentication_result = Gitlab::Auth::Result.new
+ @authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
new file mode 100644
index 00000000000..6a1f468ba5a
--- /dev/null
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -0,0 +1,64 @@
+class Profiles::ChatNamesController < Profiles::ApplicationController
+ before_action :chat_name_token, only: [:new]
+ before_action :chat_name_params, only: [:new, :create, :deny]
+
+ def index
+ @chat_names = current_user.chat_names
+ end
+
+ def new
+ end
+
+ def create
+ new_chat_name = current_user.chat_names.new(chat_name_params)
+
+ if new_chat_name.save
+ flash[:notice] = "Authorized #{new_chat_name.chat_name}"
+ else
+ flash[:alert] = "Could not authorize chat nickname. Try again!"
+ end
+
+ delete_chat_name_token
+ redirect_to profile_chat_names_path
+ end
+
+ def deny
+ delete_chat_name_token
+
+ flash[:notice] = "Denied authorization of chat nickname #{chat_name_params[:user_name]}."
+
+ redirect_to profile_chat_names_path
+ end
+
+ def destroy
+ @chat_name = chat_names.find(params[:id])
+
+ if @chat_name.destroy
+ flash[:notice] = "Deleted chat nickname: #{@chat_name.chat_name}!"
+ else
+ flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
+ end
+
+ redirect_to profile_chat_names_path
+ end
+
+ private
+
+ def delete_chat_name_token
+ chat_name_token.delete
+ end
+
+ def chat_name_params
+ @chat_name_params ||= chat_name_token.get || render_404
+ end
+
+ def chat_name_token
+ return render_404 unless params[:token] || render_404
+
+ @chat_name_token ||= Gitlab::ChatNameToken.new(params[:token])
+ end
+
+ def chat_names
+ @chat_names ||= current_user.chat_names
+ end
+end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index f71e0a1302b..f0c71725ea8 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -26,7 +26,15 @@ class ProfilesController < Profiles::ApplicationController
def reset_private_token
if current_user.reset_authentication_token!
- flash[:notice] = "Token was successfully updated"
+ flash[:notice] = "Private token was successfully reset"
+ end
+
+ redirect_to profile_account_path
+ end
+
+ def reset_incoming_email_token
+ if current_user.reset_incoming_email_token!
+ flash[:notice] = "Incoming email token was successfully reset"
end
redirect_to profile_account_path
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index b78cc6585ba..56ced786311 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -42,7 +42,7 @@ class Projects::BlobController < Projects::ApplicationController
after_edit_path =
if from_merge_request && @target_branch == @ref
diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
- "#file-path-#{hexdigest(@path)}"
+ "##{hexdigest(@path)}"
else
namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 2de8ada3e29..6b9f37983c4 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -4,7 +4,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:new, :create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
def index
@sort = params[:sort].presence || sort_value_name
@@ -62,6 +62,13 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
+ def destroy_all_merged
+ DeleteMergedBranchesService.new(@project, current_user).async_execute
+
+ redirect_to namespace_project_branches_path(@project.namespace, @project),
+ notice: 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
+ end
+
private
def ref
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
new file mode 100644
index 00000000000..13b3eec761f
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -0,0 +1,65 @@
+module Projects
+ module CycleAnalytics
+ class EventsController < Projects::ApplicationController
+ include CycleAnalyticsParams
+
+ before_action :authorize_read_cycle_analytics!
+ before_action :authorize_read_build!, only: [:test, :staging]
+ before_action :authorize_read_issue!, only: [:issue, :production]
+ before_action :authorize_read_merge_request!, only: [:code, :review]
+
+ def issue
+ render_events(events.issue_events)
+ end
+
+ def plan
+ render_events(events.plan_events)
+ end
+
+ def code
+ render_events(events.code_events)
+ end
+
+ def test
+ options[:branch] = events_params[:branch_name]
+
+ render_events(events.test_events)
+ end
+
+ def review
+ render_events(events.review_events)
+ end
+
+ def staging
+ render_events(events.staging_events)
+ end
+
+ def production
+ render_events(events.production_events)
+ end
+
+ private
+
+ def render_events(events_list)
+ respond_to do |format|
+ format.html
+ format.json { render json: { events: events_list } }
+ 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 }
+ end
+
+ def events_params
+ return {} unless params[:events].present?
+
+ params[:events].slice(:start_date, :branch_name)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 16a7b1fc6e2..96eb75a0547 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -1,11 +1,12 @@
class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::DateHelper
include ActionView::Helpers::TextHelper
+ include CycleAnalyticsParams
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+ @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
respond_to do |format|
format.html
@@ -15,14 +16,6 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
private
- def parse_start_date
- case cycle_analytics_params[:start_date]
- when '30' then 30.days.ago
- when '90' then 90.days.ago
- else 90.days.ago
- end
- end
-
def cycle_analytics_params
return {} unless params[:cycle_analytics].present?
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index ea22b2dcc15..6bd4cb3f2f5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -8,13 +8,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @all_environments = project.environments
- @environments =
- if @scope == 'stopped'
- @all_environments.stopped
- else
- @all_environments.available
+ @environments = project.environments
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: EnvironmentSerializer
+ .new(project: @project)
+ .represent(@environments)
end
+ end
end
def show
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ade01c706a7..ba46e2528e6 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -4,6 +4,7 @@ class Projects::ForksController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
+ before_action :authenticate_user!, only: [:new, :create]
def index
base_query = project.forks.includes(:creator)
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 383e184d796..3f41916e6d3 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -21,10 +21,6 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
- if project && project.public? && download_request?
- return # Allow access
- end
-
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
@@ -41,6 +37,10 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_final_spnego_response
return # Allow access
end
+ elsif project && download_request? && Guest.can?(:download_code, project)
+ @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
+
+ return # Allow access
end
send_challenges
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 662d38b10a5..13caeb42d40 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -78,11 +78,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
- if user
- access_check.allowed?
- else
- ci? || project.public?
- end
+ access_check.allowed? || ci?
end
def access
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3f1a1d1c511..4aea7bb62c4 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -69,7 +69,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
- render json: @issue.to_json(include: [:milestone, :labels])
+ render json: IssueSerializer.new.represent(@issue)
end
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 42fd09e9b7e..824ed7be73e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -3,7 +3,7 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy]
- before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
+ before_action :find_labels, only: [:index, :set_priorities, :remove_priority, :toggle_subscription]
before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
@@ -123,7 +123,10 @@ class Projects::LabelsController < Projects::ApplicationController
def label
@label ||= @project.labels.find(params[:id])
end
- alias_method :subscribable_resource, :label
+
+ def subscribable_resource
+ @available_labels.find(params[:id])
+ end
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index ece49dcd922..2d493276941 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -31,10 +31,6 @@ class Projects::LfsApiController < Projects::GitHttpClientController
private
- def objects
- @objects ||= (params[:objects] || []).to_a
- end
-
def existing_oids
@existing_oids ||= begin
storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9f104d903cc..b343ba0b744 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(:target_project)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -61,7 +60,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.html { define_discussion_vars }
format.json do
- render json: @merge_request
+ render json: MergeRequestSerializer.new.represent(@merge_request)
end
format.patch do
@@ -507,6 +506,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.close
end
+ labels
define_pipelines_vars
end
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 34318391dd9..33a152ad34f 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -5,17 +5,29 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
+ before_action :assign_commit
def show
@url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))
@commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format|
- format.html
+ format.html do
+ if @options[:extended_sha1] && !@commit
+ flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+ end
+ end
format.json do
@graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
end
end
end
+
+ def assign_commit
+ return if params[:extended_sha1].blank?
+
+ @options[:extended_sha1] = params[:extended_sha1]
+ @commit = @repo.commit(@options[:extended_sha1])
+ end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 0948ad21649..f029fde2a2f 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -146,24 +146,26 @@ class Projects::NotesController < Projects::ApplicationController
end
def note_json(note)
+ attrs = {
+ award: false,
+ id: note.id
+ }
+
if note.is_a?(AwardEmoji)
- {
+ attrs.merge!(
valid: note.valid?,
award: true,
- id: note.id,
name: note.name
- }
+ )
elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
- attrs = {
+ attrs.merge!(
valid: true,
- id: note.id,
discussion_id: note.discussion_id,
html: note_html(note),
- award: false,
note: note.note
- }
+ )
if note.diff_note?
discussion = note.to_discussion
@@ -188,15 +190,14 @@ class Projects::NotesController < Projects::ApplicationController
attrs[:original_discussion_id] = note.original_discussion_id
end
end
-
- attrs
else
- {
+ attrs.merge!(
valid: false,
- award: false,
errors: note.errors
- }
+ )
end
+
+ attrs
end
def authorize_admin_note!
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 371cc3787fb..533af80aee0 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
- @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
+ @pipeline = Ci::CreatePipelineService
+ .new(project, current_user, create_params)
+ .execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 97e6e9471e0..30c2a5d9982 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -10,8 +10,7 @@ class Projects::ServicesController < Projects::ApplicationController
layout "project_settings"
def index
- @project.build_missing_services
- @services = @project.services.visible.reload
+ @services = @project.find_or_initialize_services
end
def edit
@@ -29,6 +28,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
+ return render_404 unless @service.can_test?
+
data = @service.test_data(project, current_user)
outcome = @service.test(data)
@@ -46,6 +47,6 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service
- @service ||= @project.services.find { |service| service.to_param == params[:id] }
+ @service ||= @project.find_or_initialize_service(params[:id])
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 6988527a3be..a8a18b4fa16 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -2,9 +2,9 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
- before_action :authenticate_user!, except: [:show, :activity, :refs]
- before_action :project, except: [:new, :create]
- before_action :repository, except: [:new, :create]
+ before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
+ before_action :project, except: [:index, :new, :create]
+ before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
@@ -160,6 +160,13 @@ class ProjectsController < Projects::ApplicationController
end
end
+ def new_issue_address
+ return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
+
+ current_user.reset_incoming_email_token!
+ render json: { new_issue_address: @project.new_issue_address(current_user) }
+ end
+
def archive
return access_denied! unless can?(current_user, :archive_project, @project)
@@ -318,26 +325,44 @@ class ProjectsController < Projects::ApplicationController
end
def project_params
- project_feature_attributes =
- {
- project_feature_attributes:
- [
- :issues_access_level, :builds_access_level,
- :wiki_access_level, :merge_requests_access_level,
- :snippets_access_level, :repository_access_level
- ]
- }
+ params.require(:project)
+ .permit(project_params_ce)
+ end
- params.require(:project).permit(
- :name, :path, :description, :issues_tracker, :tag_list, :runners_token,
+ def project_params_ce
+ [
+ :avatar,
+ :build_allow_git_fetch,
+ :build_coverage_regex,
+ :build_timeout_in_minutes,
:container_registry_enabled,
- :issues_tracker_id, :default_branch,
- :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
- :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled,
+ :default_branch,
+ :description,
+ :import_url,
+ :issues_tracker,
+ :issues_tracker_id,
+ :last_activity_at,
+ :lfs_enabled,
+ :name,
+ :namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
- :lfs_enabled, project_feature_attributes
- )
+ :only_allow_merge_if_build_succeeds,
+ :path,
+ :public_builds,
+ :request_access_enabled,
+ :runners_token,
+ :tag_list,
+ :visibility_level,
+
+ project_feature_attributes: %i[
+ builds_access_level
+ issues_access_level
+ merge_requests_access_level
+ repository_access_level
+ snippets_access_level
+ wiki_access_level
+ ]
+ ]
end
def repo_exists?
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d01e0dedf52..b666aa01d6b 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -16,7 +16,7 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
- return if params[:search].nil? || params[:search].blank?
+ return if params[:search].blank?
@search_term = params[:search]
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
index 3085ff33aba..04c36b3ebfe 100644
--- a/app/controllers/sent_notifications_controller.rb
+++ b/app/controllers/sent_notifications_controller.rb
@@ -12,7 +12,7 @@ class SentNotificationsController < ApplicationController
def unsubscribe_and_redirect
noteable = @sent_notification.noteable
- noteable.unsubscribe(@sent_notification.recipient)
+ noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
flash[:notice] = "You have been unsubscribed from this thread."
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 6a881b271d7..6e29f1e8a65 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -86,7 +86,7 @@ class UsersController < ApplicationController
end
def exists
- render json: { exists: Namespace.where(path: params[:username].downcase).any? }
+ render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
end
private
@@ -104,8 +104,7 @@ class UsersController < ApplicationController
end
def contributions_calendar
- @contributions_calendar ||= Gitlab::ContributionsCalendar.
- new(contributed_projects, user)
+ @contributions_calendar ||= Gitlab::ContributionsCalendar.new(user, current_user)
end
def load_events
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index cc2073081b5..6297b2db369 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -61,31 +61,26 @@ class IssuableFinder
def project
return @project if defined?(@project)
- if project?
- @project = Project.find(params[:project_id])
+ project = Project.find(params[:project_id])
+ project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
- unless Ability.allowed?(current_user, :read_project, @project)
- @project = nil
- end
- else
- @project = nil
- end
-
- @project
+ @project = project
end
def projects
return @projects if defined?(@projects)
+ return @projects = project if project?
- if project?
- @projects = project
- elsif current_user && params[:authorized_only].presence && !current_user_related?
- @projects = current_user.authorized_projects.reorder(nil)
- elsif group
- @projects = GroupProjectsFinder.new(group).execute(current_user).reorder(nil)
- else
- @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
- end
+ projects =
+ if current_user && params[:authorized_only].presence && !current_user_related?
+ current_user.authorized_projects
+ elsif group
+ GroupProjectsFinder.new(group).execute(current_user)
+ else
+ ProjectsFinder.new.execute(current_user)
+ end
+
+ @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
end
def search
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 865f093f04a..fa0e2a5e3d8 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -6,7 +6,7 @@ class LabelsFinder < UnionFinder
def execute(skip_authorization: false)
@skip_authorization = skip_authorization
- items = find_union(label_ids, Label)
+ items = find_union(label_ids, Label) || Label.none
items = with_title(items)
sort(items)
end
@@ -18,9 +18,11 @@ class LabelsFinder < UnionFinder
def label_ids
label_ids = []
- if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project?
+ if project
+ label_ids << project.group.labels if project.group.present?
+ label_ids << project.labels
+ end
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -40,16 +42,16 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group_id
- params[:group_id].presence
+ def group?
+ params[:group_id].present?
end
- def project_id
- params[:project_id].presence
+ def project?
+ params[:project_id].present?
end
- def projects_ids
- params[:project_ids]
+ def projects?
+ params[:project_ids].present?
end
def title
@@ -59,8 +61,9 @@ class LabelsFinder < UnionFinder
def project
return @project if defined?(@project)
- if project_id
- @project = find_project
+ if project?
+ @project = Project.find(params[:project_id])
+ @project = nil unless authorized_to_read_labels?(@project)
else
@project = nil
end
@@ -68,26 +71,20 @@ class LabelsFinder < UnionFinder
@project
end
- def find_project
- if skip_authorization
- Project.find_by(id: project_id)
- else
- available_projects.find_by(id: project_id)
- end
- end
-
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : available_projects
- @projects = @projects.in_namespace(group_id) if group_id
- @projects = @projects.where(id: projects_ids) if projects_ids
+ @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
+ @projects = @projects.in_namespace(params[:group_id]) if group?
+ @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
@projects
end
- def available_projects
- @available_projects ||= ProjectsFinder.new.execute(current_user)
+ def authorized_to_read_labels?(project)
+ return true if skip_authorization
+
+ Ability.allowed?(current_user, :read_label, project)
end
end
diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb
new file mode 100644
index 00000000000..5d27d30eaa3
--- /dev/null
+++ b/app/helpers/accounts_helper.rb
@@ -0,0 +1,5 @@
+module AccountsHelper
+ def incoming_email_token_enabled?
+ current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation?
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ebd78bf9888..c816b616631 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -151,7 +151,6 @@ module ApplicationHelper
# time - Time object
# placement - Tooltip placement String (default: "top")
# html_class - Custom class for `time` element (default: "time_ago")
- # skip_js - When true, exclude the `script` tag (default: false)
#
# By default also includes a `script` element with Javascript necessary to
# initialize the `timeago` jQuery extension. If this method is called many
@@ -163,22 +162,19 @@ module ApplicationHelper
# `html_class` argument is provided.
#
# Returns an HTML-safe String
- def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false)
+ def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false)
css_classes = short_format ? 'js-short-timeago' : 'js-timeago'
css_classes << " #{html_class}" unless html_class.blank?
- css_classes << ' js-timeago-pending' unless skip_js
element = content_tag :time, time.to_s,
class: css_classes,
- datetime: time.to_time.getutc.iso8601,
title: time.to_time.in_time_zone.to_s(:medium),
- data: { toggle: 'tooltip', placement: placement, container: 'body' }
-
- unless skip_js
- element << javascript_tag(
- "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
- )
- end
+ datetime: time.to_time.getutc.iso8601,
+ data: {
+ toggle: 'tooltip',
+ placement: placement,
+ container: 'body'
+ }
element
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 45a567a1eba..be5e0301a43 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -100,4 +100,8 @@ module ApplicationSettingsHelper
options_for_select(options, @application_setting.repository_storages)
end
+
+ def sidekiq_queue_options_for_select
+ options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cd4d778e508..92bac149313 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -3,7 +3,7 @@ module AuthHelper
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
- Gitlab.config.ldap.enabled
+ Gitlab::LDAP::Config.enabled?
end
def omniauth_enabled?
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index f3aaff9140d..fde297c588e 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -5,4 +5,14 @@ module BuildsHelper
build_class += ' retried' if build.retried?
build_class
end
+
+ def javascript_build_options
+ {
+ page_url: namespace_project_build_url(@project.namespace, @project, @build),
+ build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
+ build_status: @build.status,
+ build_stage: @build.stage,
+ state1: @build.trace_with_state[:state]
+ }
+ end
end
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
new file mode 100644
index 00000000000..8893209b314
--- /dev/null
+++ b/app/helpers/components_helper.rb
@@ -0,0 +1,9 @@
+module ComponentsHelper
+ def gitlab_workhorse_version
+ if request.headers['Gitlab-Workhorse'].present?
+ request.headers['Gitlab-Workhorse'].split('-').first
+ else
+ Gitlab::Workhorse.version
+ end
+ end
+end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 0725c3f4c56..f489f9aa0d6 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -51,12 +51,11 @@ module DiffHelper
html.html_safe
end
- def diff_line_content(line, line_type = nil)
+ def diff_line_content(line)
if line.blank?
- " &nbsp;".html_safe
+ "&nbsp;".html_safe
else
- line[0] = ' ' if %w[new old].include?(line_type)
- line
+ line.sub(/^[\-+ ]/, '').html_safe
end
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
new file mode 100644
index 00000000000..27975b7ddb7
--- /dev/null
+++ b/app/helpers/environment_helper.rb
@@ -0,0 +1,29 @@
+module EnvironmentHelper
+ def environment_for_build(project, build)
+ return unless build.environment
+
+ project.environments.find_by(name: build.expanded_environment_name)
+ end
+
+ def environment_link_for_build(project, build)
+ environment = environment_for_build(project, build)
+ if environment
+ link_to environment.name, namespace_project_environment_path(project.namespace, project, environment)
+ else
+ content_tag :span, build.expanded_environment_name
+ end
+ end
+
+ def deployment_link(deployment)
+ return unless deployment
+
+ link_to "##{deployment.iid}", [deployment.project.namespace.becomes(Namespace), deployment.project, deployment.deployable]
+ end
+
+ def last_deployment_link_for_environment_build(project, build)
+ environment = environment_for_build(project, build)
+ return unless environment
+
+ deployment_link(environment.last_deployment)
+ end
+end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
new file mode 100644
index 00000000000..515e802e01e
--- /dev/null
+++ b/app/helpers/environments_helper.rb
@@ -0,0 +1,7 @@
+module EnvironmentsHelper
+ def environments_list_data
+ {
+ endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json)
+ }
+ end
+end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 00e64076408..f1a0b929d82 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -86,7 +86,7 @@ module EventsHelper
elsif event.merge_request?
namespace_project_merge_request_url(event.project.namespace,
event.project, event.merge_request)
- elsif event.note? && event.commit_note?
+ elsif event.commit_note?
namespace_project_commit_url(event.project.namespace, event.project,
event.note_target)
elsif event.note?
@@ -127,7 +127,7 @@ module EventsHelper
end
def event_note_target_path(event)
- if event.note? && event.commit_note?
+ if event.commit_note?
namespace_project_commit_path(event.project.namespace,
event.project,
event.note_target,
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index bccf64d1aac..af9087d8326 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -82,6 +82,10 @@ module GitlabRoutingHelper
namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
end
+ def pipeline_path(pipeline, *args)
+ namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
+ end
+
def milestone_path(entity, *args)
namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8127c3f3ee3..8bebda07787 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -30,11 +30,6 @@ module IssuablesHelper
end
end
- def can_add_template?(issuable)
- names = issuable_templates(issuable)
- names.empty? && can?(current_user, :push_code, @project) && !@project.private?
- end
-
def template_dropdown_tag(issuable, &block)
title = selected_template(issuable) || "Choose a template"
options = {
@@ -141,8 +136,19 @@ module IssuablesHelper
html.html_safe
end
+ def cached_assigned_issuables_count(assignee, issuable_type, state)
+ cache_key = hexdigest(['assigned_issuables_count', assignee.id, issuable_type, state].join('-'))
+ Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
+ assigned_issuables_count(assignee, issuable_type, state)
+ end
+ end
+
private
+ def assigned_issuables_count(assignee, issuable_type, state)
+ assignee.public_send("assigned_#{issuable_type}").public_send(state).count
+ end
+
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
@@ -165,9 +171,11 @@ module IssuablesHelper
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
- issuables_finder.params[:state] = state
+
+ params = issuables_finder.params.merge(state: state)
+ finder = issuables_finder.class.new(issuables_finder.current_user, params)
- issuables_finder.execute.page(1).total_count
+ finder.execute.page(1).total_count
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 221a84b042f..4f180456b16 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -68,14 +68,6 @@ module LabelsHelper
end
end
- def toggle_subscription_data(label)
- return unless label.is_a?(ProjectLabel)
-
- {
- url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
- }
- end
-
def render_colored_label(label, label_suffix = '', tooltip: true)
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
@@ -148,20 +140,24 @@ module LabelsHelper
end
end
- def label_subscription_status(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
- end
+ def label_subscription_status(label, project)
+ return 'project-level' if label.subscribed?(current_user, project)
+ return 'group-level' if label.subscribed?(current_user)
+
+ 'unsubscribed'
end
- def label_subscription_toggle_button_text(label)
- case label
- when GroupLabel then 'Subscribing to group labels is currently not supported.'
- when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ def group_label_unsubscribe_path(label, project)
+ case label_subscription_status(label, project)
+ when 'project-level' then toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)
+ when 'group-level' then toggle_subscription_group_label_path(label.group, label)
end
end
+ def label_subscription_toggle_button_text(label, project)
+ label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe'
+ end
+
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index 95b60aeab5f..2425c3a8bc8 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,6 +1,6 @@
module LfsHelper
include Gitlab::Routing.url_helpers
-
+
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
@@ -27,7 +27,11 @@ module LfsHelper
def lfs_download_access?
return false unless project.lfs_enabled?
- project.public? || ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code?
+ end
+
+ def objects
+ @objects ||= (params[:objects] || []).to_a
end
def user_can_download_code?
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 7e8369d0a05..03cc8f2b6bd 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -74,4 +74,13 @@ module NotificationsHelper
return unless notification_setting.source_type
hidden_field_tag "#{notification_setting.source_type.downcase}_id", notification_setting.source_id
end
+
+ def notification_event_name(event)
+ case event
+ when :success_pipeline
+ 'Successful pipeline'
+ else
+ event.to_s.humanize
+ end
+ end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index a46f2c6e17d..6e68aad4cb7 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -50,7 +50,7 @@ module PreferencesHelper
end
def default_project_view
- return 'readme' unless current_user
+ return anonymous_project_view unless current_user
user_view = current_user.project_view
@@ -66,4 +66,8 @@ module PreferencesHelper
"customize_workflow"
end
end
+
+ def anonymous_project_view
+ @project.empty_repo? || !can?(current_user, :download_code, @project) ? 'activity' : 'readme'
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 42c00ec3cd5..17123b1eaee 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -49,7 +49,7 @@ module ProjectsHelper
end
end
- def project_title(project, name = nil, url = nil)
+ def project_title(project)
namespace_link =
if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group))
@@ -66,10 +66,7 @@ module ProjectsHelper
end
end
- full_title = "#{namespace_link} / #{project_link}".html_safe
- full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
-
- full_title
+ "#{namespace_link} / #{project_link}".html_safe
end
def remove_project_message(project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index aba3a3f9c5d..cdb9663877c 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -31,34 +31,7 @@ module SearchHelper
end
def parse_search_result(result)
- ref = nil
- filename = nil
- basename = nil
- startline = 0
-
- result.each_line.each_with_index do |line, index|
- if line =~ /^.*:.*:\d+:/
- ref, filename, startline = line.split(':')
- startline = startline.to_i - index
- extname = Regexp.escape(File.extname(filename))
- basename = filename.sub(/#{extname}$/, '')
- break
- end
- end
-
- data = ""
-
- result.each_line do |line|
- data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
- end
-
- OpenStruct.new(
- filename: filename,
- basename: basename,
- ref: ref,
- startline: startline,
- data: data
- )
+ Gitlab::ProjectSearchResults.parse_search_result(result)
end
private
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 3d4abf76419..9bab140e60a 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -17,6 +17,8 @@ module ServicesHelper
"Event will be triggered when a build status changes"
when "wiki_page"
"Event will be triggered when a wiki page is created/updated"
+ when "commit"
+ "Event will be triggered when a commit is created/updated"
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index a9db8bb2b82..09c69786791 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -61,6 +61,10 @@ module TodosHelper
}
end
+ def todos_filter_empty?
+ todos_filter_params.values.none?
+ end
+
def todos_filter_path(options = {})
without = options.delete(:without)
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index 8cad994d10f..b0135ea2e95 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,5 +1,13 @@
module TriggersHelper
- def builds_trigger_url(project_id)
- "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ def builds_trigger_url(project_id, ref: nil)
+ if ref.nil?
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
+ else
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/ref/#{ref}/trigger/builds"
+ end
+ end
+
+ def service_trigger_url(service)
+ "#{Settings.gitlab.url}/api/v3/projects/#{service.project_id}/services/#{service.to_param}/trigger"
end
end
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index 601c8b5cd62..9460a6cd2be 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -1,22 +1,27 @@
module Emails
module Pipelines
- def pipeline_success_email(pipeline, to)
- pipeline_mail(pipeline, to, 'succeeded')
+ def pipeline_success_email(pipeline, recipients)
+ pipeline_mail(pipeline, recipients, 'succeeded')
end
- def pipeline_failed_email(pipeline, to)
- pipeline_mail(pipeline, to, 'failed')
+ def pipeline_failed_email(pipeline, recipients)
+ pipeline_mail(pipeline, recipients, 'failed')
end
private
- def pipeline_mail(pipeline, to, status)
+ def pipeline_mail(pipeline, recipients, status)
@project = pipeline.project
@pipeline = pipeline
@merge_request = pipeline.merge_requests.first
add_headers
- mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
+ # We use bcc here because we don't want to generate this emails for a
+ # thousand times. This could be potentially expensive in a loop, and
+ # recipients would contain all project watchers so it could be a lot.
+ mail(bcc: recipients,
+ subject: pipeline_subject(status),
+ skip_premailer: true) do |format|
format.html { render layout: false }
format.text
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bb60cc8736c..bf463a3b6bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
+ serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
+ validates :sidekiq_throttling_factor,
+ numericality: { greater_than: 0, less_than: 1 },
+ presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
+ validates :sidekiq_throttling_queues,
+ presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
+ if: :sidekiq_throttling_enabled?
+
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
+ sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
@@ -192,6 +203,10 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
+ def sidekiq_throttling_column_exists?
+ ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled)
+ end
+
def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
end
@@ -245,6 +260,12 @@ class ApplicationSetting < ActiveRecord::Base
ensure_health_check_access_token!
end
+ def sidekiq_throttling_enabled?
+ return false unless sidekiq_throttling_column_exists?
+
+ sidekiq_throttling_enabled
+ end
+
private
def check_repository_storages
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
new file mode 100644
index 00000000000..f321db75eeb
--- /dev/null
+++ b/app/models/chat_name.rb
@@ -0,0 +1,12 @@
+class ChatName < ActiveRecord::Base
+ belongs_to :service
+ belongs_to :user
+
+ validates :user, presence: true
+ validates :service, presence: true
+ validates :team_id, presence: true
+ validates :chat_id, presence: true
+
+ validates :user_id, uniqueness: { scope: [:service_id] }
+ validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf5f92f8462..83c0c64e5fb 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,6 +7,8 @@ module Ci
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
+ has_many :deployments, as: :deployable
+
serialize :options
serialize :yaml_variables
@@ -68,7 +70,11 @@ module Ci
environment: build.environment,
status_event: 'enqueue'
)
- MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(build.project, nil)
+ .close(new_build)
+
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
@@ -125,6 +131,34 @@ module Ci
!self.pipeline.statuses.latest.include?(self)
end
+ def expanded_environment_name
+ ExpandVariables.expand(environment, variables) if environment
+ end
+
+ def has_environment?
+ self.environment.present?
+ end
+
+ def starts_environment?
+ has_environment? && self.environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment? && self.environment_action == 'stop'
+ end
+
+ def environment_action
+ self.options.fetch(:environment, {}).fetch(:action, 'start')
+ end
+
+ def outdated_deployment?
+ success? && !last_deployment.try(:last?)
+ end
+
+ def last_deployment
+ deployments.last
+ end
+
def depends_on_builds
# Get builds of the same type
latest_builds = self.pipeline.builds.latest
@@ -271,6 +305,7 @@ module Ci
def append_trace(trace_part, offset)
recreate_trace_dir
+ touch if needs_touch?
trace_part = hide_secrets(trace_part)
@@ -280,6 +315,10 @@ module Ci
end
end
+ def needs_touch?
+ Time.now - updated_at > 15.minutes.to_i
+ end
+
def trace_file_path
if has_old_trace_file?
old_path_to_trace
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3ab19938c0f..cd7d8fd3af7 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -81,6 +81,12 @@ module Ci
PipelineHooksWorker.perform_async(id)
end
end
+
+ after_transition any => [:success, :failed] do |pipeline|
+ pipeline.run_after_commit do
+ PipelineNotificationWorker.perform_async(pipeline.id)
+ end
+ end
end
scope :latest, -> { order(id: :desc) }
@@ -113,6 +119,11 @@ module Ci
project.id
end
+ # For now the only user who participates is the user who triggered
+ def participants(_current_user = nil)
+ Array(user)
+ end
+
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 613444e0d70..69d8afc45da 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -183,6 +183,10 @@ module Issuable
grouping_columns
end
+
+ def to_ability_name
+ model_name.singular
+ end
end
def today?
@@ -211,7 +215,7 @@ module Issuable
end
end
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
participants(user).include?(user)
end
@@ -244,7 +248,18 @@ module Issuable
# issuable.class # => MergeRequest
# issuable.to_ability_name # => "merge_request"
def to_ability_name
- self.class.to_s.underscore
+ self.class.to_ability_name
+ end
+
+ # Convert this Issuable class name to a format usable by notifications.
+ #
+ # Examples:
+ #
+ # issuable.class # => MergeRequest
+ # issuable.human_class_name # => "merge request"
+
+ def human_class_name
+ @human_class_name ||= self.class.name.titleize.downcase
end
# Returns a Hash of attributes to be used for Twitter card metadata
@@ -286,6 +301,11 @@ module Issuable
false
end
+ def assignee_or_author?(user)
+ # We're comparing IDs here so we don't need to load any associations.
+ author_id == user.id || assignee_id == user.id
+ end
+
def record_metrics
metrics = self.metrics || create_metrics
metrics.record!
diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb
new file mode 100644
index 00000000000..50a1d7fc3e1
--- /dev/null
+++ b/app/models/concerns/select_for_project_authorization.rb
@@ -0,0 +1,9 @@
+module SelectForProjectAuthorization
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def select_for_project_authorization
+ select("members.user_id, projects.id AS project_id, members.access_level")
+ end
+ end
+end
diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb
index 083257f1005..83daa9b1a64 100644
--- a/app/models/concerns/subscribable.rb
+++ b/app/models/concerns/subscribable.rb
@@ -12,39 +12,71 @@ module Subscribable
has_many :subscriptions, dependent: :destroy, as: :subscribable
end
- def subscribed?(user)
- if subscription = subscriptions.find_by_user_id(user.id)
+ def subscribed?(user, project = nil)
+ if subscription = subscriptions.find_by(user: user, project: project)
subscription.subscribed
else
- subscribed_without_subscriptions?(user)
+ subscribed_without_subscriptions?(user, project)
end
end
# Override this method to define custom logic to consider a subscribable as
# subscribed without an explicit subscription record.
- def subscribed_without_subscriptions?(user)
+ def subscribed_without_subscriptions?(user, project)
false
end
- def subscribers
- subscriptions.where(subscribed: true).map(&:user)
+ def subscribers(project)
+ subscriptions_available(project).
+ where(subscribed: true).
+ map(&:user)
end
- def toggle_subscription(user)
- subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: !subscribed?(user))
+ def toggle_subscription(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project).
+ update(subscribed: !subscribed?(user, project))
+ end
+
+ def subscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: true)
+ end
+
+ def unsubscribe(user, project = nil)
+ unsubscribe_from_other_levels(user, project)
+
+ find_or_initialize_subscription(user, project)
+ .update(subscribed: false)
end
- def subscribe(user)
+ private
+
+ def unsubscribe_from_other_levels(user, project)
+ other_subscriptions = subscriptions.where(user: user)
+
+ other_subscriptions =
+ if project.blank?
+ other_subscriptions.where.not(project: nil)
+ else
+ other_subscriptions.where(project: nil)
+ end
+
+ other_subscriptions.update_all(subscribed: false)
+ end
+
+ def find_or_initialize_subscription(user, project)
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: true)
+ find_or_initialize_by(user_id: user.id, project_id: project.try(:id))
end
- def unsubscribe(user)
+ def subscriptions_available(project)
+ t = Subscription.arel_table
+
subscriptions.
- find_or_initialize_by(user_id: user.id).
- update(subscribed: false)
+ where(t[:project_id].eq(nil).or(t[:project_id].eq(project.try(:id))))
end
end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 24c7b26d223..04d30f46210 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -4,17 +4,21 @@ module TokenAuthenticatable
private
def write_new_token(token_field)
- new_token = generate_token(token_field)
+ new_token = generate_available_token(token_field)
write_attribute(token_field, new_token)
end
- def generate_token(token_field)
+ def generate_available_token(token_field)
loop do
- token = Devise.friendly_token
+ token = generate_token(token_field)
break token unless self.class.unscoped.find_by(token_field => token)
end
end
+ def generate_token(token_field)
+ Devise.friendly_token
+ end
+
class_methods do
def authentication_token_fields
@token_fields || []
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index 8ed4a56b19b..314a1ce9b63 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -1,12 +1,8 @@
class CycleAnalytics
- include Gitlab::Database::Median
- include Gitlab::Database::DateTime
-
- DEPLOYMENT_METRIC_STAGES = %i[production staging]
-
def initialize(project, from:)
@project = project
@from = from
+ @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end
def summary
@@ -14,90 +10,46 @@ class CycleAnalytics
end
def issue
- calculate_metric(: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]])
end
def plan
- calculate_metric(: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
- calculate_metric(:code,
+ @fetcher.calculate_metric(:code,
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
MergeRequest.arel_table[:created_at])
end
def test
- calculate_metric(:test,
+ @fetcher.calculate_metric(:test,
MergeRequest::Metrics.arel_table[:latest_build_started_at],
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
end
def review
- calculate_metric(:review,
+ @fetcher.calculate_metric(:review,
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
end
def staging
- calculate_metric(:staging,
+ @fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
def production
- calculate_metric(:production,
+ @fetcher.calculate_metric(:production,
Issue.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
-
- private
-
- 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), end_time_attrs, start_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)
- arel_table = MergeRequestsClosingIssues.arel_table
-
- # Load issues
- query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
- join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
- where(Issue.arel_table[:project_id].eq(@project.id)).
- where(Issue.arel_table[:deleted_at].eq(nil)).
- where(Issue.arel_table[:created_at].gteq(@from))
-
- # Load merge_requests
- query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
- on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
- join(MergeRequest::Metrics.arel_table).
- on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
-
- if DEPLOYMENT_METRIC_STAGES.include?(name)
- # Limit to merge requests that have been deployed to production after `@from`
- query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
- end
-
- query
- end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 73f415c0ef0..5278efd71d2 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -37,6 +37,10 @@ class Environment < ActiveRecord::Base
state :stopped
end
+ def recently_updated_on_branch?(ref)
+ ref.to_s == last_deployment.try(:ref)
+ end
+
def last_deployment
deployments.last
end
@@ -92,6 +96,7 @@ class Environment < ActiveRecord::Base
def stop!(current_user)
return unless stoppable?
+ stop
stop_action.play(current_user)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 43e67069b70..21eaca917b8 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -49,6 +49,7 @@ class Event < ActiveRecord::Base
update_all(updated_at: Time.now)
end
+ # Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type in (?) AND action in (?))",
Event::PUSHED, ["MergeRequest", "Issue"],
@@ -61,8 +62,8 @@ class Event < ActiveRecord::Base
end
def visible_to_user?(user = nil)
- if push?
- true
+ if push? || commit_note?
+ Ability.allowed?(user, :download_code, project)
elsif membership_changed?
true
elsif created_project?
@@ -282,7 +283,7 @@ class Event < ActiveRecord::Base
end
def commit_note?
- target.for_commit?
+ note? && target && target.for_commit?
end
def issue_note?
@@ -294,7 +295,7 @@ class Event < ActiveRecord::Base
end
def project_snippet_note?
- target.for_snippet?
+ note? && target && target.for_snippet?
end
def note_target
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index fd9a8c1b8b7..91b508eb325 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -29,6 +29,15 @@ class ExternalIssue
@project
end
+ def project_id
+ @project.id
+ end
+
+ # Pattern used to extract `JIRA-123` issue references from text
+ def self.reference_pattern
+ @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
+ end
+
def to_reference(_from_project = nil)
id
end
diff --git a/app/models/group.rb b/app/models/group.rb
index d9e90cd256a..73b0f1c6572 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -5,6 +5,7 @@ class Group < Namespace
include Gitlab::VisibilityLevel
include AccessRequestable
include Referable
+ include SelectForProjectAuthorization
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :group_members
@@ -61,6 +62,14 @@ class Group < Namespace
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
+
+ def select_for_project_authorization
+ if current_scope.joins_values.include?(:shared_projects)
+ select("members.user_id, projects.id AS project_id, project_group_links.group_access")
+ else
+ super
+ end
+ end
end
def to_reference(_from_project = nil)
@@ -176,4 +185,8 @@ class Group < Namespace
def system_hook_service
SystemHooksService.new
end
+
+ def refresh_members_authorized_projects
+ UserProjectAccessChangedService.new(users.pluck(:id)).execute
+ end
end
diff --git a/app/models/guest.rb b/app/models/guest.rb
new file mode 100644
index 00000000000..01285ca1264
--- /dev/null
+++ b/app/models/guest.rb
@@ -0,0 +1,7 @@
+class Guest
+ class << self
+ def can?(action, subject)
+ Ability.allowed?(nil, action, subject)
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 4f02b02c488..6e8f5d3c422 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -250,29 +250,9 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
- user ? readable_by?(user) : publicly_visible?
- end
-
- # Returns `true` if the given User can read the current Issue.
- def readable_by?(user)
- if user.admin?
- true
- elsif project.owner == user
- true
- elsif confidential?
- author == user ||
- assignee == user ||
- project.team.member?(user, Gitlab::Access::REPORTER)
- else
- project.public? ||
- project.internal? && !user.external? ||
- project.team.member?(user)
- end
- end
+ return false unless project.feature_available?(:issues, user)
- # Returns `true` if this Issue is visible to everybody.
- def publicly_visible?
- project.public? && !confidential?
+ user ? readable_by?(user) : publicly_visible?
end
def overdue?
@@ -286,7 +266,7 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
- json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+ json[:subscribed] = subscribed?(options[:user], project) if options.has_key?(:user) && options[:user]
if options.has_key?(:labels)
json[:labels] = labels.as_json(
@@ -297,4 +277,32 @@ class Issue < ActiveRecord::Base
end
end
end
+
+ private
+
+ # Returns `true` if the given User can read the current Issue.
+ #
+ # This method duplicates the same check of issue_policy.rb
+ # for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
+ # Make sure to sync this method with issue_policy.rb
+ def readable_by?(user)
+ if user.admin?
+ true
+ elsif project.owner == user
+ true
+ elsif confidential?
+ author == user ||
+ assignee == user ||
+ project.team.member?(user, Gitlab::Access::REPORTER)
+ else
+ project.public? ||
+ project.internal? && !user.external? ||
+ project.team.member?(user)
+ end
+ end
+
+ # Returns `true` if this Issue is visible to everybody.
+ def publicly_visible?
+ project.public? && !confidential?
+ end
end
diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb
new file mode 100644
index 00000000000..f0b7d9914c8
--- /dev/null
+++ b/app/models/issue_collection.rb
@@ -0,0 +1,42 @@
+# IssueCollection can be used to reduce a list of issues down to a subset.
+#
+# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
+# to take a list of issues and return a new list of issues based on some
+# criteria. For example, given a list of issues you may want to return a list of
+# issues that can be read or updated by a given user.
+class IssueCollection
+ attr_reader :collection
+
+ def initialize(collection)
+ @collection = collection
+ end
+
+ # Returns all the issues that can be updated by the user.
+ def updatable_by_user(user)
+ return collection if user.admin?
+
+ # Given all the issue projects we get a list of projects that the current
+ # user has at least reporter access to.
+ projects_with_reporter_access = user.
+ projects_with_reporter_access_limited_to(project_ids).
+ pluck(:id)
+
+ collection.select do |issue|
+ if projects_with_reporter_access.include?(issue.project_id)
+ true
+ elsif issue.is_a?(Issue)
+ issue.assignee_or_author?(user)
+ else
+ false
+ end
+ end
+ end
+
+ alias_method :visible_to, :updatable_by_user
+
+ private
+
+ def project_ids
+ @project_ids ||= collection.map(&:project_id).uniq
+ end
+end
diff --git a/app/models/key.rb b/app/models/key.rb
index 568a60b8af3..ff8dda2dc89 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -6,7 +6,7 @@ class Key < ActiveRecord::Base
belongs_to :user
- before_validation :strip_white_space, :generate_fingerprint
+ before_validation :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
@@ -21,8 +21,9 @@ class Key < ActiveRecord::Base
after_destroy :remove_from_shell
after_destroy :post_destroy_hook
- def strip_white_space
- self.key = key.strip unless key.blank?
+ def key=(value)
+ value.strip! unless value.blank?
+ write_attribute(:key, value)
end
def publishable_key
diff --git a/app/models/member.rb b/app/models/member.rb
index b89ba8ecbb8..7be2665bf48 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -113,6 +113,8 @@ class Member < ActiveRecord::Base
member.save
end
+ UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User)
+
member
end
@@ -239,6 +241,7 @@ class Member < ActiveRecord::Base
end
def post_create_hook
+ UserProjectAccessChangedService.new(user.id).execute
system_hook_service.execute_hooks_for(self, :create)
end
@@ -247,9 +250,19 @@ class Member < ActiveRecord::Base
end
def post_destroy_hook
+ refresh_member_authorized_projects
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def refresh_member_authorized_projects
+ # If user/source is being destroyed, project access are gonna be destroyed eventually
+ # because of DB foreign keys, so we shouldn't bother with refreshing after each
+ # member is destroyed through association
+ return if destroyed_by_association.present?
+
+ UserProjectAccessChangedService.new(user_id).execute
+ end
+
def after_accept_invite
post_create_hook
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index d76feb9680e..9d3eab52189 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -692,12 +692,15 @@ class MergeRequest < ActiveRecord::Base
def environments
return [] unless diff_head_commit
- @environments ||=
- begin
- envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
- envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
- envs.uniq
- end
+ @environments ||= begin
+ target_envs = target_project.environments_for(
+ target_branch, commit: diff_head_commit, with_tags: true)
+
+ source_envs = source_project.environments_for(
+ source_branch, commit: diff_head_commit) if source_project
+
+ (target_envs.to_a + source_envs.to_a).uniq
+ end
end
def state_human_name
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
index 99c49a020c9..cdc408738be 100644
--- a/app/models/merge_request/metrics.rb
+++ b/app/models/merge_request/metrics.rb
@@ -1,5 +1,6 @@
class MergeRequest::Metrics < ActiveRecord::Base
belongs_to :merge_request
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
def record!
if merge_request.merged? && self.merged_at.blank?
diff --git a/app/models/note.rb b/app/models/note.rb
index 2d644b03e4d..9ff5e308ed2 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -7,6 +7,7 @@ class Note < ActiveRecord::Base
include Importable
include FasterCacheKeys
include CacheMarkdownField
+ include AfterCommitQueue
cache_markdown_field :note, pipeline: :note
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 121b598b8f3..43fc218de2b 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -32,7 +32,9 @@ class NotificationSetting < ActiveRecord::Base
:reopen_merge_request,
:close_merge_request,
:reassign_merge_request,
- :merge_merge_request
+ :merge_merge_request,
+ :failed_pipeline,
+ :success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
diff --git a/app/models/project.rb b/app/models/project.rb
index 686d285410b..995359daf1e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -13,6 +13,7 @@ class Project < ActiveRecord::Base
include CaseSensitivity
include TokenAuthenticatable
include ProjectFeaturesCompatibility
+ include SelectForProjectAuthorization
extend Gitlab::ConfigHelper
@@ -23,7 +24,9 @@ class Project < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
- delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
+ delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
+ :merge_requests_enabled?, :issues_enabled?, to: :project_feature,
+ allow_nil: true
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
@@ -35,6 +38,7 @@ class Project < ActiveRecord::Base
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -74,9 +78,9 @@ class Project < ActiveRecord::Base
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event'
has_many :boards, before_add: :validate_board_limit, dependent: :destroy
+ has_many :chat_services
# Project services
- has_many :services
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
@@ -89,6 +93,7 @@ class Project < ActiveRecord::Base
has_one :assembla_service, dependent: :destroy
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
+ has_one :mattermost_slash_commands_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
@@ -207,8 +212,38 @@ class Project < ActiveRecord::Base
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
- scope :with_builds_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0') }
- scope :with_issues_enabled, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id').where('project_features.issues_access_level IS NULL or project_features.issues_access_level > 0') }
+ scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
+
+ # "enabled" here means "not disabled". It includes private features!
+ scope :with_feature_enabled, ->(feature) {
+ access_level_attribute = ProjectFeature.access_level_attribute(feature)
+ with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] })
+ }
+
+ # Picks a feature where the level is exactly that given.
+ scope :with_feature_access_level, ->(feature, level) {
+ access_level_attribute = ProjectFeature.access_level_attribute(feature)
+ with_project_feature.where(project_features: { access_level_attribute => level })
+ }
+
+ scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
+ scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
+
+ # project features may be "disabled", "internal" or "enabled". If "internal",
+ # they are only available to team members. This scope returns projects where
+ # the feature is either enabled, or internal with permission for the user.
+ def self.with_feature_available_for_user(feature, user)
+ return with_feature_enabled(feature) if user.try(:admin?)
+
+ unconditional = with_feature_access_level(feature, [nil, ProjectFeature::ENABLED])
+ return unconditional if user.nil?
+
+ conditional = with_feature_access_level(feature, ProjectFeature::PRIVATE)
+ authorized = user.authorized_projects.merge(conditional.reorder(nil))
+
+ union = Gitlab::SQL::Union.new([unconditional.select(:id), authorized.select(:id)])
+ where(arel_table[:id].in(Arel::Nodes::SqlLiteral.new(union.to_sql)))
+ end
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
@@ -624,13 +659,12 @@ class Project < ActiveRecord::Base
end
def new_issue_address(author)
- # This feature is disabled for the time being.
- return nil
+ return unless Gitlab::IncomingEmail.supports_issue_creation? && author
- if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
- Gitlab::IncomingEmail.reply_address(
- "#{path_with_namespace}+#{author.authentication_token}")
- end
+ author.ensure_incoming_email_token!
+
+ Gitlab::IncomingEmail.reply_address(
+ "#{path_with_namespace}+#{author.incoming_email_token}")
end
def build_commit_note(commit)
@@ -719,27 +753,32 @@ class Project < ActiveRecord::Base
update_column(:has_external_wiki, services.external_wikis.any?)
end
- def build_missing_services
+ def find_or_initialize_services
services_templates = Service.where(template: true)
- Service.available_services_names.each do |service_name|
+ Service.available_services_names.map do |service_name|
service = find_service(services, service_name)
- # If service is available but missing in db
- if service.nil?
+ if service
+ service
+ else
# We should check if template for the service exists
template = find_service(services_templates, service_name)
if template.nil?
- # If no template, we should create an instance. Ex `create_gitlab_ci_service`
- public_send("create_#{service_name}_service")
+ # If no template, we should create an instance. Ex `build_gitlab_ci_service`
+ public_send("build_#{service_name}_service")
else
- Service.create_from_template(self.id, template)
+ Service.build_from_template(id, template)
end
end
end
end
+ def find_or_initialize_service(name)
+ find_or_initialize_services.find { |service| service.to_param == name }
+ end
+
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
@@ -849,7 +888,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || !repository.has_visible_content?
+ repository.empty_repo?
end
def repo
@@ -1255,16 +1294,10 @@ class Project < ActiveRecord::Base
# Checks if `user` is authorized for this project, with at least the
# `min_access_level` (if given).
- #
- # If you change the logic of this method, please also update `User#authorized_projects`
def authorized_for_user?(user, min_access_level = nil)
return false unless user
- return true if personal? && namespace_id == user.namespace_id
-
- authorized_for_user_by_group?(user, min_access_level) ||
- authorized_for_user_by_members?(user, min_access_level) ||
- authorized_for_user_by_shared_projects?(user, min_access_level)
+ user.authorized_project?(self, min_access_level)
end
def append_or_update_attribute(name, value)
@@ -1289,22 +1322,30 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit, with_tags: false)
- environment_ids = deployments.group(:environment_id).
- select(:environment_id)
+ def environments_for(ref, commit: nil, with_tags: false)
+ deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
- environment_ids =
- if with_tags
- environment_ids.where('ref=? OR tag IS TRUE', ref)
- else
- environment_ids.where(ref: ref)
- end
+ environment_ids = deployments
+ .where(deployments_query, ref.to_s)
+ .group(:environment_id)
+ .select(:environment_id)
- environments.available.where(id: environment_ids).select do |environment|
+ environments_found = environments.available
+ .where(id: environment_ids).to_a
+
+ return environments_found unless commit
+
+ environments_found.select do |environment|
environment.includes_commit?(commit)
end
end
+ def environments_recently_updated_on_branch(branch)
+ environments_for(branch).select do |environment|
+ environment.recently_updated_on_branch?(branch)
+ end
+ end
+
private
def pushes_since_gc_redis_key
@@ -1316,30 +1357,6 @@ class Project < ActiveRecord::Base
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
- def authorized_for_user_by_group?(user, min_access_level)
- member = user.group_members.find_by(source_id: group)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_members?(user, min_access_level)
- member = members.find_by(user_id: user)
-
- member && (!min_access_level || member.access_level >= min_access_level)
- end
-
- def authorized_for_user_by_shared_projects?(user, min_access_level)
- shared_projects = user.group_members.joins(group: :shared_projects).
- where(project_group_links: { project_id: self })
-
- if min_access_level
- members_scope = { access_level: Gitlab::Access.values.select { |access| access >= min_access_level } }
- shared_projects = shared_projects.where(members: members_scope)
- end
-
- shared_projects.any?
- end
-
# Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
new file mode 100644
index 00000000000..a00d43773d9
--- /dev/null
+++ b/app/models/project_authorization.rb
@@ -0,0 +1,8 @@
+class ProjectAuthorization < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :project
+
+ validates :project, presence: true
+ validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index b37ce1d3cf6..03194fc2141 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -20,6 +20,15 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds repository)
+ class << self
+ def access_level_attribute(feature)
+ feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name)
+ raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature)
+
+ "#{feature}_access_level".to_sym
+ end
+ end
+
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
@@ -35,29 +44,26 @@ class ProjectFeature < ActiveRecord::Base
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
- raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
-
- get_permission(user, public_send("#{feature}_access_level"))
+ access_level = public_send(ProjectFeature.access_level_attribute(feature))
+ get_permission(user, access_level)
end
def builds_enabled?
- return true unless builds_access_level
-
builds_access_level > DISABLED
end
def wiki_enabled?
- return true unless wiki_access_level
-
wiki_access_level > DISABLED
end
def merge_requests_enabled?
- return true unless merge_requests_access_level
-
merge_requests_access_level > DISABLED
end
+ def issues_enabled?
+ issues_access_level > DISABLED
+ end
+
private
# Validates builds and merge requests access level
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index db46def11eb..6149c35cc61 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -16,6 +16,9 @@ class ProjectGroupLink < ActiveRecord::Base
validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true
validate :different_group
+ after_create :refresh_group_members_authorized_projects
+ after_destroy :refresh_group_members_authorized_projects
+
def self.access_options
Gitlab::Access.options
end
@@ -35,4 +38,8 @@ class ProjectGroupLink < ActiveRecord::Base
errors.add(:base, "Project cannot be shared with the project it is in.")
end
end
+
+ def refresh_group_members_authorized_projects
+ group.refresh_members_authorized_projects
+ end
end
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
new file mode 100644
index 00000000000..d36beff5fa6
--- /dev/null
+++ b/app/models/project_services/chat_service.rb
@@ -0,0 +1,21 @@
+# Base class for Chat services
+# This class is not meant to be used directly, but only to inherrit from.
+class ChatService < Service
+ default_value_for :category, 'chat'
+
+ has_many :chat_names, foreign_key: :service_id
+
+ def valid_token?(token)
+ self.respond_to?(:token) &&
+ self.token.present? &&
+ ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
+ end
+
+ def supported_events
+ []
+ end
+
+ def trigger(params)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2dbe0075465..2caf6179ef8 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,24 +1,3 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-# template :boolean default(FALSE)
-# push_events :boolean default(TRUE)
-# issues_events :boolean default(TRUE)
-# merge_requests_events :boolean default(TRUE)
-# tag_push_events :boolean default(TRUE)
-# note_events :boolean default(TRUE), not null
-# build_events :boolean default(FALSE), not null
-#
-
class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers
@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ def supported_events
+ %w(commit merge_request)
+ end
+
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
@@ -70,7 +53,7 @@ class JiraService < IssueTrackerService
end
def jira_project
- @jira_project ||= client.Project.find(project_key)
+ @jira_project ||= jira_request { client.Project.find(project_key) }
end
def help
@@ -128,14 +111,25 @@ class JiraService < IssueTrackerService
# we just want to test settings
test_settings
else
- close_issue(push, issue)
+ jira_issue = jira_request { client.Issue.find(issue.iid) }
+
+ return false unless jira_issue.present?
+
+ close_issue(push, jira_issue)
end
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_key = mentioned.id
+ unless can_cross_reference?(noteable)
+ return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
+ end
+
+ jira_issue = jira_request { client.Issue.find(mentioned.id) }
+
+ return unless jira_issue.present?
+
project = self.project
- noteable_name = noteable.class.name.underscore.downcase
+ noteable_name = noteable.model_name.singular
noteable_id = if noteable.is_a?(Commit)
noteable.id
else
@@ -160,7 +154,7 @@ class JiraService < IssueTrackerService
}
}
- add_comment(data, issue_key)
+ add_comment(data, jira_issue)
end
# reason why service cannot be tested
@@ -181,16 +175,22 @@ class JiraService < IssueTrackerService
def test_settings
return unless url.present?
# Test settings by getting the project
- jira_project
-
- rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
- false
+ jira_request { jira_project.present? }
end
private
+ def can_cross_reference?(noteable)
+ case noteable
+ when Commit then commit_events
+ when MergeRequest then merge_requests_events
+ else true
+ end
+ end
+
def close_issue(entity, issue)
+ return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
+
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
@@ -200,55 +200,85 @@ class JiraService < IssueTrackerService
commit_url = build_entity_url(:commit, commit_id)
# Depending on the JIRA project's workflow, a comment during transition
- # may or may not be allowed. Split the operation in to two calls so the
- # comment always works.
- transition_issue(issue)
- add_issue_solved_comment(issue, commit_id, commit_url)
+ # may or may not be allowed. Refresh the issue after transition and check
+ # if it is closed, so we don't have one comment for every commit.
+ issue = jira_request { client.Issue.find(issue.key) } if transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url) if issue.resolution
end
def transition_issue(issue)
- issue = client.Issue.find(issue.iid)
issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = "Issue solved with [#{commit_id}|#{commit_url}]."
- send_message(issue.iid, comment)
+ link_title = "GitLab: Solved by commit #{commit_id}."
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ link_props = build_remote_link_props(url: commit_url, title: link_title, resolved: true)
+ send_message(issue, comment, link_props)
end
- def add_comment(data, issue_key)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
- entity_name = data[:entity][:name]
- entity_url = data[:entity][:url]
+ def add_comment(data, issue)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
+ link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
+ link_props = build_remote_link_props(url: entity_url, title: link_title)
- unless comment_exists?(issue_key, message)
- send_message(issue_key, message)
+ unless comment_exists?(issue, message)
+ send_message(issue, message, link_props)
end
end
- def comment_exists?(issue_key, message)
- comments = client.Issue.find(issue_key).comments
- comments.map { |comment| comment.body.include?(message) }.any?
+ def comment_exists?(issue, message)
+ comments = jira_request { issue.comments }
+
+ comments.present? && comments.any? { |comment| comment.body.include?(message) }
end
- def send_message(issue_key, message)
+ def send_message(issue, message, remote_link_props)
return unless url.present?
- issue = client.Issue.find(issue_key)
+ jira_request do
+ if issue.comments.build.save!(body: message)
+ remote_link = issue.remotelink.build
+ remote_link.save!(remote_link_props)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ end
- if issue.comments.build.save!(body: message)
- result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
+ Rails.logger.info(result_message)
+ result_message
end
+ end
- Rails.logger.info(result_message)
- result_message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
- Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ # Build remote link on JIRA properties
+ # Icons here must be available on WEB so JIRA can read the URL
+ # We are using a open word graphics icon which have LGPL license
+ def build_remote_link_props(url:, title:, resolved: false)
+ status = {
+ resolved: resolved
+ }
+
+ if resolved
+ status[:icon] = {
+ title: 'Closed',
+ url16x16: 'http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png'
+ }
+ end
+
+ {
+ GlobalID: 'GitLab',
+ object: {
+ url: url,
+ title: title,
+ status: status,
+ icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ }
+ }
end
def resource_url(resource)
@@ -256,16 +286,23 @@ class JiraService < IssueTrackerService
end
def build_entity_url(entity_name, entity_id)
- resource_url(
- polymorphic_url(
- [
- self.project.namespace.becomes(Namespace),
- self.project,
- entity_name
- ],
- id: entity_id,
- routing_type: :path
- )
+ polymorphic_url(
+ [
+ self.project.namespace.becomes(Namespace),
+ self.project,
+ entity_name
+ ],
+ id: entity_id,
+ host: Settings.gitlab.base_url
)
end
+
+ # Handle errors when doing JIRA API calls
+ def jira_request
+ yield
+
+ rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
+ nil
+ end
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
new file mode 100644
index 00000000000..67902329593
--- /dev/null
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -0,0 +1,56 @@
+class MattermostSlashCommandsService < ChatService
+ include TriggersHelper
+
+ prop_accessor :token
+
+ def can_test?
+ false
+ end
+
+ def title
+ 'Mattermost Command'
+ end
+
+ def description
+ "Perform common operations on GitLab in Mattermost"
+ end
+
+ def to_param
+ 'mattermost_slash_commands'
+ end
+
+ def help
+ "This service allows you to use slash commands with your Mattermost installation.<br/>
+ To setup this Service you need to create a new <b>Slash commands</b> in your Mattermost integration panel.<br/>
+ <br/>
+ Create integration with URL #{service_trigger_url(self)} and enter the token below."
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: '' }
+ ]
+ end
+
+ def trigger(params)
+ return nil unless valid_token?(params[:token])
+
+ user = find_chat_user(params)
+ unless user
+ url = authorize_chat_name_url(params)
+ return Mattermost::Presenter.authorize_chat_name(url)
+ end
+
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ end
+
+ private
+
+ def find_chat_user(params)
+ ChatNames::FindUserService.new(self, params).execute
+ end
+
+ def authorize_chat_name_url(params)
+ ChatNames::AuthorizeUserService.new(self, params).execute
+ end
+end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index ec3c1bc85ee..745f9bd1b43 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -1,10 +1,7 @@
class PipelinesEmailService < Service
prop_accessor :recipients
- boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines
- validates :recipients,
- presence: true,
- if: ->(s) { s.activated? && !s.add_pusher? }
+ validates :recipients, presence: true, if: :activated?
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
@@ -34,8 +31,8 @@ class PipelinesEmailService < Service
return unless all_recipients.any?
- pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
- Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
+ pipeline_id = data[:object_attributes][:id]
+ PipelineNotificationWorker.new.perform(pipeline_id, all_recipients)
end
def can_test?
@@ -58,9 +55,6 @@ class PipelinesEmailService < Service
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
- name: 'add_pusher',
- label: 'Add pusher to recipients list' },
- { type: 'checkbox',
name: 'notify_only_broken_pipelines' },
]
end
@@ -85,12 +79,6 @@ class PipelinesEmailService < Service
end
def retrieve_recipients(data)
- all_recipients = recipients.to_s.split(',').reject(&:blank?)
-
- if add_pusher? && data[:user].try(:[], :email)
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ recipients.to_s.split(',').reject(&:blank?)
end
end
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 9e84e90f38c..797c5937f09 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -46,25 +46,25 @@ class SlackService
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
commented_on_message(
- "[commit #{commit_sha}](#{@note_url})",
+ "commit #{commit_sha}",
format_title(commit[:message]))
end
def create_issue_note(issue)
commented_on_message(
- "[issue ##{issue[:iid]}](#{@note_url})",
+ "issue ##{issue[:iid]}",
format_title(issue[:title]))
end
def create_merge_note(merge_request)
commented_on_message(
- "[merge request !#{merge_request[:iid]}](#{@note_url})",
+ "merge request !#{merge_request[:iid]}",
format_title(merge_request[:title]))
end
def create_snippet_note(snippet)
commented_on_message(
- "[snippet ##{snippet[:id]}](#{@note_url})",
+ "snippet ##{snippet[:id]}",
format_title(snippet[:title]))
end
@@ -76,8 +76,8 @@ class SlackService
"[#{@project_name}](#{@project_url})"
end
- def commented_on_message(target_link, title)
- @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
+ def commented_on_message(target, title)
+ @message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*"
end
end
end
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
index f06b3562965..f8d03c0e2fa 100644
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ b/app/models/project_services/slack_service/pipeline_message.rb
@@ -1,11 +1,10 @@
class SlackService
class PipelineMessage < BaseMessage
- attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+ attr_reader :ref_type, :ref, :status, :project_name, :project_url,
:user_name, :duration, :pipeline_id
def initialize(data)
pipeline_attributes = data[:object_attributes]
- @sha = pipeline_attributes[:sha]
@ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
@ref = pipeline_attributes[:ref]
@status = pipeline_attributes[:status]
@@ -14,7 +13,7 @@ class SlackService
@project_name = data[:project][:path_with_namespace]
@project_url = data[:project][:web_url]
- @user_name = data[:commit] && data[:commit][:author_name]
+ @user_name = data[:user] && data[:user][:name]
end
def pretext
@@ -73,7 +72,7 @@ class SlackService
end
def pipeline_link
- "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+ "[##{pipeline_id}](#{pipeline_url})"
end
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 46f70da2452..9db96347322 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -127,7 +127,7 @@ class ProjectWiki
end
def search_files(query)
- repository.search_files(query, default_branch)
+ repository.search_files_by_content(query, default_branch)
end
def repository
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 30be7262438..31be06be50c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -15,16 +15,6 @@ class Repository
Gitlab.config.repositories.storages
end
- def self.remove_storage_from_path(repo_path)
- storages.find do |_, storage_path|
- if repo_path.start_with?(storage_path)
- return repo_path.sub(storage_path, '')
- end
- end
-
- repo_path
- end
-
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -84,15 +74,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
+
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
+
commit = ::Commit.new(commit, @project) if commit
commit
- rescue Rugged::OdbError
+ rescue Rugged::OdbError, Rugged::TreeError
nil
end
@@ -184,11 +176,18 @@ class Repository
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.tags.create(tag_name, target, options)
+ rugged.tags.create(tag_name, target, options)
+ tag = find_tag(tag_name)
+
+ GitHooksService.new.execute(user, path_to_repo, oldrev, tag.target, ref) do
+ # we already created a tag, because we need tag SHA to pass correct
+ # values to hooks
end
- find_tag(tag_name)
+ tag
+ rescue GitHooksService::PreReceiveError
+ rugged.tags.delete(tag_name)
+ raise
end
def rm_branch(user, branch_name)
@@ -232,6 +231,8 @@ class Repository
def ref_exists?(ref)
rugged.references.exist?(ref)
+ rescue Rugged::ReferenceError
+ false
end
def update_ref!(name, newrev, oldrev)
@@ -239,7 +240,7 @@ class Repository
# offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
- command = %w[git update-ref --stdin -z]
+ command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
_, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
end
@@ -270,11 +271,7 @@ class Repository
end
def kept_around?(sha)
- begin
- ref_exists?(keep_around_ref_name(sha))
- rescue Rugged::ReferenceError
- false
- end
+ ref_exists?(keep_around_ref_name(sha))
end
def tag_names
@@ -631,7 +628,7 @@ class Repository
@head_tree ||= Tree.new(self, head_commit.sha, nil)
end
- def tree(sha = :head, path = nil)
+ def tree(sha = :head, path = nil, recursive: false)
if sha == :head
if path.nil?
return head_tree
@@ -640,7 +637,7 @@ class Repository
end
end
- Tree.new(self, sha, path)
+ Tree.new(self, sha, path, recursive: recursive)
end
def blob_at_branch(branch_name, path)
@@ -1063,12 +1060,25 @@ class Repository
merge_base(ancestor_id, descendant_id) == ancestor_id
end
- def search_files(query, ref)
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ def search_files_by_content(query, ref)
+ return [] if empty_repo? || query.blank?
+
offset = 2
args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
+ def search_files_by_name(query, ref)
+ return [] if empty_repo? || query.blank?
+
+ args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+ Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
diff --git a/app/models/service.rb b/app/models/service.rb
index 625fbc48302..0c36acfc1b7 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
+ default_value_for :commit_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -202,7 +203,6 @@ class Service < ActiveRecord::Base
bamboo
buildkite
builds_email
- pipelines_email
bugzilla
campfire
custom_issue_tracker
@@ -214,6 +214,8 @@ class Service < ActiveRecord::Base
hipchat
irker
jira
+ mattermost_slash_commands
+ pipelines_email
pivotaltracker
pushover
redmine
@@ -222,11 +224,11 @@ class Service < ActiveRecord::Base
]
end
- def self.create_from_template(project_id, template)
+ def self.build_from_template(project_id, template)
service = template.dup
service.template = false
service.project_id = project_id
- service if service.save
+ service
end
private
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index 3b8aa1eb866..17869c8bac2 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,8 +1,9 @@
class Subscription < ActiveRecord::Base
belongs_to :user
+ belongs_to :project
belongs_to :subscribable, polymorphic: true
- validates :user_id,
- uniqueness: { scope: [:subscribable_id, :subscribable_type] },
- presence: true
+ validates :user, :subscribable, presence: true
+
+ validates :project_id, uniqueness: { scope: [:subscribable_id, :subscribable_type, :user_id] }
end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 7c4ed6e393b..2d1d68dbd81 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -3,15 +3,16 @@ class Tree
attr_accessor :repository, :sha, :path, :entries
- def initialize(repository, sha, path = '/')
+ def initialize(repository, sha, path = '/', recursive: false)
path = '/' if path.blank?
@repository = repository
@sha = sha
@path = path
+ @recursive = recursive
git_repo = @repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
end
def readme
@@ -58,4 +59,21 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
+
+ private
+
+ def get_entries(git_repo, sha, path, recursive: false)
+ current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
+ end
+ end
+
+ ordered_entries
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 65e96ee6b2e..29fb849940a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -13,6 +13,7 @@ class User < ActiveRecord::Base
DEFAULT_NOTIFICATION_LEVEL = :participating
add_authentication_token_field :authentication_token
+ add_authentication_token_field :incoming_email_token
default_value_for :admin, false
default_value_for(:external) { current_application_settings.user_default_external }
@@ -55,6 +56,7 @@ class User < ActiveRecord::Base
has_many :personal_access_tokens, dependent: :destroy
has_many :identities, dependent: :destroy, autosave: true
has_many :u2f_registrations, dependent: :destroy
+ has_many :chat_names, dependent: :destroy
# Groups
has_many :members, dependent: :destroy
@@ -71,6 +73,8 @@ class User < ActiveRecord::Base
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
+ has_many :project_authorizations, dependent: :destroy
+ has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
@@ -119,7 +123,7 @@ class User < ActiveRecord::Base
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
- before_save :ensure_authentication_token
+ before_save :ensure_authentication_token, :ensure_incoming_email_token
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
@@ -172,7 +176,7 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
- scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
+ scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
def self.with_two_factor
@@ -225,19 +229,19 @@ class User < ActiveRecord::Base
def filter(filter_name)
case filter_name
when 'admins'
- self.admins
+ admins
when 'blocked'
- self.blocked
+ blocked
when 'two_factor_disabled'
- self.without_two_factor
+ without_two_factor
when 'two_factor_enabled'
- self.with_two_factor
+ with_two_factor
when 'wop'
- self.without_projects
+ without_projects
when 'external'
- self.external
+ external
else
- self.active
+ active
end
end
@@ -335,7 +339,7 @@ class User < ActiveRecord::Base
end
def generate_password
- if self.force_random_password
+ if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end
end
@@ -376,56 +380,55 @@ class User < ActiveRecord::Base
end
def two_factor_otp_enabled?
- self.otp_required_for_login?
+ otp_required_for_login?
end
def two_factor_u2f_enabled?
- self.u2f_registrations.exists?
+ u2f_registrations.exists?
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
- return if self.errors.key?(:username) &&
- self.errors[:username].include?('has already been taken')
+ return if errors.key?(:username) &&
+ errors[:username].include?('has already been taken')
- namespace_name = self.username
- existing_namespace = Namespace.by_path(namespace_name)
- if existing_namespace && existing_namespace != self.namespace
- self.errors.add(:username, 'has already been taken')
+ existing_namespace = Namespace.by_path(username)
+ if existing_namespace && existing_namespace != namespace
+ errors.add(:username, 'has already been taken')
end
end
def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ unless avatar.image?
+ errors.add :avatar, "only images allowed"
end
end
def unique_email
- if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
- self.errors.add(:email, 'has already been taken')
+ if !emails.exists?(email: email) && Email.exists?(email: email)
+ errors.add(:email, 'has already been taken')
end
end
def owns_notification_email
- return if self.temp_oauth_email?
+ return if temp_oauth_email?
- self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
+ errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end
def owns_public_email
- return if self.public_email.blank?
+ return if public_email.blank?
- self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
+ errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end
def update_emails_with_primary_email
- primary_email_record = self.emails.find_by(email: self.email)
+ primary_email_record = emails.find_by(email: email)
if primary_email_record
primary_email_record.destroy
- self.emails.create(email: self.email_was)
+ emails.create(email: email_was)
- self.update_secondary_emails!
+ update_secondary_emails!
end
end
@@ -437,16 +440,60 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
- # Returns projects user is authorized to access.
- #
- # If you change the logic of this method, please also update `Project#authorized_for_user`
+ def refresh_authorized_projects
+ loop do
+ begin
+ Gitlab::Database.serialized_transaction do
+ project_authorizations.delete_all
+
+ # project_authorizations_union can return multiple records for the same project/user with
+ # different access_level so we take row with the maximum access_level
+ project_authorizations.connection.execute <<-SQL
+ INSERT INTO project_authorizations (user_id, project_id, access_level)
+ SELECT user_id, project_id, MAX(access_level) AS access_level
+ FROM (#{project_authorizations_union.to_sql}) sub
+ GROUP BY user_id, project_id
+ SQL
+
+ update_column(:authorized_projects_populated, true) unless authorized_projects_populated
+ end
+
+ break
+ # In the event of a concurrent modification Rails raises StatementInvalid.
+ # In this case we want to keep retrying until the transaction succeeds
+ rescue ActiveRecord::StatementInvalid
+ end
+ end
+ end
+
def authorized_projects(min_access_level = nil)
- Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
+ refresh_authorized_projects unless authorized_projects_populated
+
+ # We're overriding an association, so explicitly call super with no arguments or it would be passed as `force_reload` to the association
+ projects = super()
+ projects = projects.where('project_authorizations.access_level >= ?', min_access_level) if min_access_level
+
+ projects
+ end
+
+ def authorized_project?(project, min_access_level = nil)
+ authorized_projects(min_access_level).exists?({ id: project.id })
+ end
+
+ # Returns the projects this user has reporter (or greater) access to, limited
+ # to at most the given projects.
+ #
+ # This method is useful when you have a list of projects and want to
+ # efficiently check to which of these projects the user has at least reporter
+ # access.
+ def projects_with_reporter_access_limited_to(projects)
+ authorized_projects(Gitlab::Access::REPORTER).where(id: projects)
end
def viewable_starred_projects
- starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (#{projects_union.to_sql})",
- [Project::PUBLIC, Project::INTERNAL])
+ starred_projects.where("projects.visibility_level IN (?) OR projects.id IN (?)",
+ [Project::PUBLIC, Project::INTERNAL],
+ authorized_projects.select(:project_id))
end
def owned_projects
@@ -569,7 +616,7 @@ class User < ActiveRecord::Base
end
def project_deploy_keys
- DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id)
+ DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end
def accessible_deploy_keys
@@ -585,38 +632,38 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
- %w(name username skype linkedin twitter).each do |attr|
- value = self.send(attr)
- self.send("#{attr}=", Sanitize.clean(value)) if value.present?
+ %w[name username skype linkedin twitter].each do |attr|
+ value = public_send(attr)
+ public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end
end
def set_notification_email
- if self.notification_email.blank? || !self.all_emails.include?(self.notification_email)
- self.notification_email = self.email
+ if notification_email.blank? || !all_emails.include?(notification_email)
+ self.notification_email = email
end
end
def set_public_email
- if self.public_email.blank? || !self.all_emails.include?(self.public_email)
+ if public_email.blank? || !all_emails.include?(public_email)
self.public_email = ''
end
end
def update_secondary_emails!
- self.set_notification_email
- self.set_public_email
- self.save if self.notification_email_changed? || self.public_email_changed?
+ set_notification_email
+ set_public_email
+ save if notification_email_changed? || public_email_changed?
end
def set_projects_limit
# `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard!
- return unless self.has_attribute?(:projects_limit)
+ return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed?
- return unless self.projects_limit.nil? || connection_default_value_defined
+ return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit
end
@@ -646,7 +693,7 @@ class User < ActiveRecord::Base
def with_defaults
User.defaults.each do |k, v|
- self.send("#{k}=", v)
+ public_send("#{k}=", v)
end
self
@@ -666,7 +713,7 @@ class User < ActiveRecord::Base
# Thus it will automatically generate a new fragment
# when the event is updated because the key changes.
def reset_events_cache
- Event.where(author_id: self.id).
+ Event.where(author_id: id).
order('id DESC').limit(1000).
update_all(updated_at: Time.now)
end
@@ -699,8 +746,8 @@ class User < ActiveRecord::Base
def all_emails
all_emails = []
- all_emails << self.email unless self.temp_oauth_email?
- all_emails.concat(self.emails.map(&:email))
+ all_emails << email unless temp_oauth_email?
+ all_emails.concat(emails.map(&:email))
all_emails
end
@@ -714,21 +761,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct
# Ensure user has namespace
- self.create_namespace!(path: self.username, name: self.username) unless self.namespace
+ create_namespace!(path: username, name: username) unless namespace
- if self.username_changed?
- self.namespace.update_attributes(path: self.username, name: self.username)
+ if username_changed?
+ namespace.update_attributes(path: username, name: username)
end
end
def post_create_hook
- log_info("User \"#{self.name}\" (#{self.email}) was created")
- notification_service.new_user(self, @reset_token) if self.created_by_id
+ log_info("User \"#{name}\" (#{email}) was created")
+ notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
- log_info("User \"#{self.name}\" (#{self.email}) was removed")
+ log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
@@ -772,7 +819,7 @@ class User < ActiveRecord::Base
end
def oauth_authorized_tokens
- Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
+ Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end
# Returns the projects a user contributed to in the last year.
@@ -876,16 +923,14 @@ class User < ActiveRecord::Base
private
- def projects_union(min_access_level = nil)
- relations = [personal_projects.select(:id),
- groups_projects.select(:id),
- projects.select(:id),
- groups.joins(:shared_projects).select(:project_id)]
-
- if min_access_level
- scope = { access_level: Gitlab::Access.all_values.select { |access| access >= min_access_level } }
- relations = [relations.shift] + relations.map { |relation| relation.where(members: scope) }
- end
+ # Returns a union query of projects that the user is authorized to access
+ def project_authorizations_union
+ relations = [
+ personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::OWNER} AS access_level"),
+ groups_projects.select_for_project_authorization,
+ projects.select_for_project_authorization,
+ groups.joins(:shared_projects).select_for_project_authorization
+ ]
Gitlab::SQL::Union.new(relations)
end
@@ -905,7 +950,7 @@ class User < ActiveRecord::Base
end
def ensure_external_user_rights
- return unless self.external?
+ return unless external?
self.can_create_group = false
self.projects_limit = 0
@@ -917,7 +962,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist
- if domain_matches?(blocked_domains, self.email)
+ if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.'
valid = false
end
@@ -925,7 +970,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank?
- if domain_matches?(allowed_domains, self.email)
+ if domain_matches?(allowed_domains, email)
valid = true
else
error = "domain is not authorized for sign-up"
@@ -933,7 +978,7 @@ class User < ActiveRecord::Base
end
end
- self.errors.add(:email, error) unless valid
+ errors.add(:email, error) unless valid
valid
end
@@ -946,4 +991,13 @@ class User < ActiveRecord::Base
signup_domain =~ regexp
end
end
+
+ def generate_token(token_field)
+ if token_field == :incoming_email_token
+ # Needs to be all lowercase and alphanumeric because it's gonna be used in an email address.
+ SecureRandom.hex.to_i(16).to_s(36)
+ else
+ super
+ end
+ end
end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 2232e231cf8..8b25332b73c 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -5,7 +5,7 @@ module Ci
# If we can't read build we should also not have that
# ability when looking at this in context of commit_status
- %w(read create update admin).each do |rule|
+ %w[read create update admin].each do |rule|
cannot! :"#{rule}_commit_status" unless can? :"#{rule}_build"
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
new file mode 100644
index 00000000000..3d2eef1c50c
--- /dev/null
+++ b/app/policies/ci/pipeline_policy.rb
@@ -0,0 +1,4 @@
+module Ci
+ class PipelinePolicy < BuildPolicy
+ end
+end
diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb
index c253f9a9399..9501e499507 100644
--- a/app/policies/issuable_policy.rb
+++ b/app/policies/issuable_policy.rb
@@ -4,7 +4,7 @@ class IssuablePolicy < BasePolicy
end
def rules
- if @user && (@subject.author == @user || @subject.assignee == @user)
+ if @user && @subject.assignee_or_author?(@user)
can! :"read_#{action_name}"
can! :"update_#{action_name}"
end
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index bd1811a3c54..88f3179c6ff 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -1,4 +1,8 @@
class IssuePolicy < IssuablePolicy
+ # This class duplicates the same check of Issue#readable_by? for performance reasons
+ # Make sure to sync this class checks with issue.rb to avoid security problems.
+ # Check commit 002ad215818450d2cbbc5fa065850a953dc7ada8 for more information.
+
def issue
@subject
end
@@ -8,9 +12,8 @@ class IssuePolicy < IssuablePolicy
if @subject.confidential? && !can_read_confidential?
cannot! :read_issue
- cannot! :admin_issue
cannot! :update_issue
- cannot! :read_issue
+ cannot! :admin_issue
end
end
@@ -18,11 +21,7 @@ class IssuePolicy < IssuablePolicy
def can_read_confidential?
return false unless @user
- return true if @user.admin?
- return true if @subject.author == @user
- return true if @subject.assignee == @user
- return true if @subject.project.team.member?(@user, Gitlab::Access::REPORTER)
- false
+ IssueCollection.new([@subject]).visible_to(@user).any?
end
end
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
new file mode 100644
index 00000000000..5fdf2bbf7c3
--- /dev/null
+++ b/app/serializers/analytics_build_entity.rb
@@ -0,0 +1,40 @@
+class AnalyticsBuildEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :name
+ expose :id
+ expose :ref, as: :branch
+ expose :short_sha
+ expose :author, using: UserEntity
+
+ expose :started_at, as: :date do |build|
+ interval_in_words(build[:started_at])
+ end
+
+ expose :duration, as: :total_time do |build|
+ distance_of_time_as_hash(build[:duration].to_f)
+ end
+
+ expose :branch do
+ expose :ref, as: :name
+
+ expose :url do |build|
+ url_to(:namespace_project_tree, build, build.ref)
+ end
+ end
+
+ expose :url do |build|
+ url_to(:namespace_project_build, build)
+ end
+
+ expose :commit_url do |build|
+ url_to(:namespace_project_commit, build, build.sha)
+ end
+
+ private
+
+ def url_to(route, build, id = nil)
+ public_send("#{route}_url", build.project.namespace, build.project, id || build)
+ end
+end
diff --git a/app/serializers/analytics_build_serializer.rb b/app/serializers/analytics_build_serializer.rb
new file mode 100644
index 00000000000..f172d67d356
--- /dev/null
+++ b/app/serializers/analytics_build_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsBuildSerializer < BaseSerializer
+ entity AnalyticsBuildEntity
+end
diff --git a/app/serializers/analytics_commit_entity.rb b/app/serializers/analytics_commit_entity.rb
new file mode 100644
index 00000000000..402cecbfd08
--- /dev/null
+++ b/app/serializers/analytics_commit_entity.rb
@@ -0,0 +1,13 @@
+class AnalyticsCommitEntity < CommitEntity
+ include EntityDateHelper
+
+ expose :short_id, as: :short_sha
+
+ expose :total_time do |commit|
+ distance_of_time_as_hash(request.total_time.to_f)
+ end
+
+ unexpose :author_name
+ unexpose :author_email
+ unexpose :message
+end
diff --git a/app/serializers/analytics_commit_serializer.rb b/app/serializers/analytics_commit_serializer.rb
new file mode 100644
index 00000000000..cdbfecf2b70
--- /dev/null
+++ b/app/serializers/analytics_commit_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsCommitSerializer < BaseSerializer
+ entity AnalyticsCommitEntity
+end
diff --git a/app/serializers/analytics_generic_serializer.rb b/app/serializers/analytics_generic_serializer.rb
new file mode 100644
index 00000000000..9f4859e8410
--- /dev/null
+++ b/app/serializers/analytics_generic_serializer.rb
@@ -0,0 +1,7 @@
+class AnalyticsGenericSerializer < BaseSerializer
+ def represent(resource, opts = {})
+ resource.symbolize_keys!
+
+ super(resource, opts)
+ end
+end
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
new file mode 100644
index 00000000000..44c50f18613
--- /dev/null
+++ b/app/serializers/analytics_issue_entity.rb
@@ -0,0 +1,29 @@
+class AnalyticsIssueEntity < Grape::Entity
+ include RequestAwareEntity
+ include EntityDateHelper
+
+ expose :title
+ expose :author, using: UserEntity
+
+ expose :iid do |object|
+ object[:iid].to_s
+ end
+
+ expose :total_time do |object|
+ distance_of_time_as_hash(object[:total_time].to_f)
+ end
+
+ expose(:created_at) do |object|
+ interval_in_words(object[:created_at])
+ end
+
+ expose :url do |object|
+ url_to(:namespace_project_issue, id: object[:iid].to_s)
+ end
+
+ private
+
+ def url_to(route, id)
+ public_send("#{route}_url", request.project.namespace, request.project, id)
+ end
+end
diff --git a/app/serializers/analytics_issue_serializer.rb b/app/serializers/analytics_issue_serializer.rb
new file mode 100644
index 00000000000..4fb3e8f1bb4
--- /dev/null
+++ b/app/serializers/analytics_issue_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsIssueSerializer < AnalyticsGenericSerializer
+ entity AnalyticsIssueEntity
+end
diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb
new file mode 100644
index 00000000000..888265eaa38
--- /dev/null
+++ b/app/serializers/analytics_merge_request_entity.rb
@@ -0,0 +1,7 @@
+class AnalyticsMergeRequestEntity < AnalyticsIssueEntity
+ expose :state
+
+ expose :url do |object|
+ url_to(:namespace_project_merge_request, id: object[:iid].to_s)
+ end
+end
diff --git a/app/serializers/analytics_merge_request_serializer.rb b/app/serializers/analytics_merge_request_serializer.rb
new file mode 100644
index 00000000000..4622a1dd855
--- /dev/null
+++ b/app/serializers/analytics_merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class AnalyticsMergeRequestSerializer < AnalyticsGenericSerializer
+ entity AnalyticsMergeRequestEntity
+end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 3d9ac66de0e..cf1c418a88e 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -4,21 +4,21 @@ class BuildEntity < Grape::Entity
expose :id
expose :name
- expose :build_url do |build|
- url_to(:namespace_project_build, build)
+ expose :build_path do |build|
+ path_to(:namespace_project_build, build)
end
- expose :retry_url do |build|
- url_to(:retry_namespace_project_build, build)
+ expose :retry_path do |build|
+ path_to(:retry_namespace_project_build, build)
end
- expose :play_url, if: ->(build, _) { build.manual? } do |build|
- url_to(:play_namespace_project_build, build)
+ expose :play_path, if: ->(build, _) { build.manual? } do |build|
+ path_to(:play_namespace_project_build, build)
end
private
- def url_to(route, build)
- send("#{route}_url", build.project.namespace, build.project, build)
+ def path_to(route, build)
+ send("#{route}_path", build.project.namespace, build.project, build)
end
end
diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb
index f7eba6fc1e3..acc20f6dc52 100644
--- a/app/serializers/commit_entity.rb
+++ b/app/serializers/commit_entity.rb
@@ -9,4 +9,11 @@ class CommitEntity < API::Entities::RepoCommit
request.project,
id: commit.id)
end
+
+ expose :commit_path do |commit|
+ namespace_project_tree_path(
+ request.project.namespace,
+ request.project,
+ id: commit.id)
+ end
end
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index ad6fc8d665b..d610fbe0c8a 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity
deployment.ref
end
- expose :ref_url do |deployment|
- namespace_project_tree_url(
+ expose :ref_path do |deployment|
+ namespace_project_tree_path(
deployment.project.namespace,
deployment.project,
id: deployment.ref)
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
new file mode 100644
index 00000000000..b333b3344c3
--- /dev/null
+++ b/app/serializers/entity_date_helper.rb
@@ -0,0 +1,35 @@
+module EntityDateHelper
+ include ActionView::Helpers::DateHelper
+
+ def interval_in_words(diff)
+ "#{distance_of_time_in_words(diff.to_f)} ago"
+ end
+
+ # Converts seconds into a hash such as:
+ # { days: 1, hours: 3, mins: 42, seconds: 40 }
+ #
+ # It returns 0 seconds for zero or negative numbers
+ # It rounds to nearest time unit and does not return zero
+ # i.e { min: 1 } instead of { mins: 1, seconds: 0 }
+ def distance_of_time_as_hash(diff)
+ diff = diff.abs.floor
+
+ return { seconds: 0 } if diff == 0
+
+ mins = (diff / 60).floor
+ seconds = diff % 60
+ hours = (mins / 60).floor
+ mins = mins % 60
+ days = (hours / 24).floor
+ hours = hours % 24
+
+ duration_hash = {}
+
+ duration_hash[:days] = days if days > 0
+ duration_hash[:hours] = hours if hours > 0
+ duration_hash[:mins] = mins if mins > 0
+ duration_hash[:seconds] = seconds if seconds > 0
+
+ duration_hash
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index ee4392cc46d..7e0fc9c071e 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -9,8 +9,15 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
- expose :environment_url do |environment|
- namespace_project_environment_url(
+ expose :environment_path do |environment|
+ namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
+ expose :stop_path do |environment|
+ stop_namespace_project_environment_path(
environment.project.namespace,
environment.project,
environment)
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
new file mode 100644
index 00000000000..17c9160cb19
--- /dev/null
+++ b/app/serializers/issuable_entity.rb
@@ -0,0 +1,16 @@
+class IssuableEntity < Grape::Entity
+ expose :id
+ expose :iid
+ expose :assignee_id
+ expose :author_id
+ expose :description
+ expose :lock_version
+ expose :milestone_id
+ expose :position
+ expose :state
+ expose :title
+ expose :updated_by_id
+ expose :created_at
+ expose :updated_at
+ expose :deleted_at
+end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
new file mode 100644
index 00000000000..6429159ebe1
--- /dev/null
+++ b/app/serializers/issue_entity.rb
@@ -0,0 +1,9 @@
+class IssueEntity < IssuableEntity
+ expose :branch_name
+ expose :confidential
+ expose :due_date
+ expose :moved_to_id
+ expose :project_id
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
+end
diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb
new file mode 100644
index 00000000000..4fff54a9126
--- /dev/null
+++ b/app/serializers/issue_serializer.rb
@@ -0,0 +1,3 @@
+class IssueSerializer < BaseSerializer
+ entity IssueEntity
+end
diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb
new file mode 100644
index 00000000000..304fd9de08f
--- /dev/null
+++ b/app/serializers/label_entity.rb
@@ -0,0 +1,11 @@
+class LabelEntity < Grape::Entity
+ expose :id
+ expose :title
+ expose :color
+ expose :description
+ expose :group_id
+ expose :project_id
+ expose :template
+ expose :created_at
+ expose :updated_at
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
new file mode 100644
index 00000000000..7445298c714
--- /dev/null
+++ b/app/serializers/merge_request_entity.rb
@@ -0,0 +1,14 @@
+class MergeRequestEntity < IssuableEntity
+ expose :in_progress_merge_commit_sha
+ expose :locked_at
+ expose :merge_commit_sha
+ expose :merge_error
+ expose :merge_params
+ expose :merge_status
+ expose :merge_user_id
+ expose :merge_when_build_succeeds
+ expose :source_branch
+ expose :source_project_id
+ expose :target_branch
+ expose :target_project_id
+end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
new file mode 100644
index 00000000000..aa6e00dfcb4
--- /dev/null
+++ b/app/serializers/merge_request_serializer.rb
@@ -0,0 +1,3 @@
+class MergeRequestSerializer < BaseSerializer
+ entity MergeRequestEntity
+end
diff --git a/app/services/after_branch_delete_service.rb b/app/services/after_branch_delete_service.rb
new file mode 100644
index 00000000000..2be4d3e6ab5
--- /dev/null
+++ b/app/services/after_branch_delete_service.rb
@@ -0,0 +1,23 @@
+require_relative 'base_service'
+
+##
+# Branch can be deleted either by DeleteBranchService
+# or by GitPushService.
+#
+class AfterBranchDeleteService < BaseService
+ attr_reader :branch_name
+
+ def execute(branch_name)
+ @branch_name = branch_name
+
+ stop_environments
+ end
+
+ private
+
+ def stop_environments
+ Ci::StopEnvironmentsService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 8ea88da8a53..c00c5aebf57 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -9,8 +9,8 @@ module Auth
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
- unless current_user || project
- return error('DENIED', status: 403, message: 'access forbidden') unless scope
+ unless scope || current_user || project
+ return error('DENIED', status: 403, message: 'access forbidden')
end
{ token: authorized_token(scope).encoded }
@@ -76,7 +76,7 @@ module Auth
case requested_action
when 'pull'
- requested_project.public? || build_can_pull?(requested_project) || user_can_pull?(requested_project)
+ build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
else
@@ -92,23 +92,23 @@ module Auth
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
- @authentication_abilities.include?(:build_read_container_image) &&
+ has_authentication_ability?(:build_read_container_image) &&
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
def user_can_pull?(requested_project)
- @authentication_abilities.include?(:read_container_image) &&
+ has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
end
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
- @authentication_abilities.include?(:build_create_container_image) &&
+ has_authentication_ability?(:build_create_container_image) &&
requested_project == project
end
def user_can_push?(requested_project)
- @authentication_abilities.include?(:create_container_image) &&
+ has_authentication_ability?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
@@ -118,5 +118,9 @@ module Auth
http_status: status
}
end
+
+ def has_authentication_ability?(capability)
+ (@authentication_abilities || []).include?(capability)
+ end
end
end
diff --git a/app/services/chat_names/authorize_user_service.rb b/app/services/chat_names/authorize_user_service.rb
new file mode 100644
index 00000000000..321bf3a9205
--- /dev/null
+++ b/app/services/chat_names/authorize_user_service.rb
@@ -0,0 +1,38 @@
+module ChatNames
+ class AuthorizeUserService
+ include Gitlab::Routing.url_helpers
+
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ return unless chat_name_params.values.all?(&:present?)
+
+ token = request_token
+
+ new_profile_chat_name_url(token: token) if token
+ end
+
+ private
+
+ def request_token
+ chat_name_token.store!(chat_name_params)
+ end
+
+ def chat_name_token
+ Gitlab::ChatNameToken.new
+ end
+
+ def chat_name_params
+ {
+ service_id: @service.id,
+ team_id: @params[:team_id],
+ team_domain: @params[:team_domain],
+ chat_id: @params[:user_id],
+ chat_name: @params[:user_name]
+ }
+ end
+ end
+end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
new file mode 100644
index 00000000000..4f5c5567b42
--- /dev/null
+++ b/app/services/chat_names/find_user_service.rb
@@ -0,0 +1,26 @@
+module ChatNames
+ class FindUserService
+ def initialize(service, params)
+ @service = service
+ @params = params
+ end
+
+ def execute
+ chat_name = find_chat_name
+ return unless chat_name
+
+ chat_name.touch(:last_used_at)
+ chat_name.user
+ end
+
+ private
+
+ def find_chat_name
+ ChatName.find_by(
+ service: @service,
+ team_id: @params[:team_id],
+ chat_id: @params[:user_id]
+ )
+ end
+ end
+end
diff --git a/app/services/ci/send_pipeline_notification_service.rb b/app/services/ci/send_pipeline_notification_service.rb
deleted file mode 100644
index ceb182801f7..00000000000
--- a/app/services/ci/send_pipeline_notification_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- class SendPipelineNotificationService
- attr_reader :pipeline
-
- def initialize(new_pipeline)
- @pipeline = new_pipeline
- end
-
- def execute(recipients)
- email_template = "pipeline_#{pipeline.status}_email"
-
- return unless Notify.respond_to?(email_template)
-
- recipients.each do |to|
- Notify.public_send(email_template, pipeline, to).deliver_later
- end
- end
- end
-end
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
new file mode 100644
index 00000000000..cf590459cb2
--- /dev/null
+++ b/app/services/ci/stop_environments_service.rb
@@ -0,0 +1,29 @@
+module Ci
+ class StopEnvironmentsService < BaseService
+ attr_reader :ref
+
+ def execute(branch_name)
+ @ref = branch_name
+
+ return unless has_ref?
+
+ environments.each do |environment|
+ next unless environment.stoppable?
+ next unless can?(current_user, :create_deployment, project)
+
+ environment.stop!(current_user)
+ end
+ end
+
+ private
+
+ def has_ref?
+ @ref.present?
+ end
+
+ def environments
+ @environments ||= project
+ .environments_recently_updated_on_branch(@ref)
+ end
+ end
+end
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
new file mode 100644
index 00000000000..8b8deafedb7
--- /dev/null
+++ b/app/services/delete_merged_branches_service.rb
@@ -0,0 +1,18 @@
+require_relative 'base_service'
+
+class DeleteMergedBranchesService < BaseService
+ def async_execute
+ DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
+
+ branches = project.repository.branch_names
+ branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
+ end
+end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 0081364b8aa..a880952e274 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -6,12 +6,10 @@ class DestroyGroupService
end
def async_execute
- group.transaction do
- # Soft delete via paranoia gem
- group.destroy
- job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
- Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
- end
+ # Soft delete via paranoia gem
+ group.destroy
+ job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
+ Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
end
def execute
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index e8415862de5..77c6c81cc1b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -49,10 +49,7 @@ class GitPushService < BaseService
update_gitattributes if is_default_branch?
end
- # Update merge requests that may be affected by this push. A new branch
- # could cause the last commit of a merge request to change.
- update_merge_requests
-
+ execute_related_hooks
perform_housekeeping
end
@@ -62,14 +59,24 @@ class GitPushService < BaseService
protected
- def update_merge_requests
- UpdateMergeRequestsWorker.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
+ def execute_related_hooks
+ # Update merge requests that may be affected by this push. A new branch
+ # could cause the last commit of a merge request to change.
+ #
+ UpdateMergeRequestsWorker
+ .perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
+
+ if push_remove_branch?
+ AfterBranchDeleteService
+ .new(project, current_user)
+ .execute(branch_name)
+ end
end
def perform_housekeeping
@@ -105,35 +112,11 @@ class GitPushService < BaseService
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages
- is_default_branch = is_default_branch?
-
- authors = Hash.new do |hash, commit|
- email = commit.author_email
- next hash[email] if hash.has_key?(email)
-
- hash[email] = commit_user(commit)
- end
+ default = is_default_branch?
@push_commits.each do |commit|
- # Keep track of the issues that will be actually closed because they are on a default branch.
- # Hence, when creating cross-reference notes, the not-closed issues (on non-default branches)
- # will also have cross-reference.
- closed_issues = []
-
- if is_default_branch
- # Close issues if these commits were pushed to the project's default branch and the commit message matches the
- # closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
- # a different branch.
- closed_issues = commit.closes_issues(current_user)
- closed_issues.each do |issue|
- if can?(current_user, :update_issue, issue)
- Issues::CloseService.new(project, authors[commit], {}).execute(issue, commit: commit)
- end
- end
- end
-
- commit.create_cross_references!(authors[commit], closed_issues)
- update_issue_metrics(commit, authors)
+ ProcessCommitWorker.
+ perform_async(project.id, current_user.id, commit.id, default)
end
end
@@ -176,11 +159,4 @@ class GitPushService < BaseService
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
-
- def update_issue_metrics(commit, authors)
- mentioned_issues = commit.all_references(authors[commit]).issues
-
- Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
- update_all(first_mentioned_in_commit_at: commit.committed_date)
- end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index bb92cd80cc9..575795788de 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -212,9 +212,9 @@ class IssuableBaseService < BaseService
def change_subscription(issuable)
case params.delete(:subscription_event)
when 'subscribe'
- issuable.subscribe(current_user)
+ issuable.subscribe(current_user, project)
when 'unsubscribe'
- issuable.unsubscribe(current_user)
+ issuable.unsubscribe(current_user, project)
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 45cca216ccc..ab4c51386a4 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,8 +1,21 @@
module Issues
class CloseService < Issues::BaseService
+ # Closes the supplied issue if the current user is able to do so.
def execute(issue, commit: nil, notifications: true, system_note: true)
return issue unless can?(current_user, :update_issue, issue)
+ close_issue(issue,
+ commit: commit,
+ notifications: notifications,
+ system_note: system_note)
+ end
+
+ # Closes the supplied issue without checking if the user is authorized to
+ # do so.
+ #
+ # The code calling this method is responsible for ensuring that a user is
+ # allowed to close the given issue.
+ def close_issue(issue, commit: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
diff --git a/app/services/merge_requests/add_todo_when_build_fails_service.rb b/app/services/merge_requests/add_todo_when_build_fails_service.rb
index d572a928a42..12a8415d9a5 100644
--- a/app/services/merge_requests/add_todo_when_build_fails_service.rb
+++ b/app/services/merge_requests/add_todo_when_build_fails_service.rb
@@ -1,13 +1,18 @@
module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails
+ #
def execute(commit_status)
+ return if commit_status.allow_failure?
+
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request)
end
end
- # Closes any pending build failed todos for the parent MRs when a build is retried
+ # Closes any pending build failed todos for the parent MRs when a
+ # build is retried
+ #
def close(commit_status)
commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index f415244068b..dd0d738674e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -48,11 +48,11 @@ module MergeRequests
end
# See if source and target branches exist
- unless merge_request.source_project.commit(merge_request.source_branch)
+ if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end
- unless merge_request.target_project.commit(merge_request.target_branch)
+ if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 22596b4014a..4a7e6930842 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -60,15 +60,7 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
- if merge_request.source_branch == @branch_name || force_push?
- merge_request.reload_diff
- else
- mr_commit_ids = merge_request.commits.map(&:id)
- push_commit_ids = @commits.map(&:id)
- matches = mr_commit_ids & push_commit_ids
- merge_request.reload_diff if matches.any?
- end
-
+ reload_diff(merge_request) unless branch_removed?
merge_request.mark_as_unchecked
end
end
@@ -173,5 +165,16 @@ module MergeRequests
def branch_removed?
Gitlab::Git.blank_ref?(@newrev)
end
+
+ def reload_diff(merge_request)
+ if merge_request.source_branch == @branch_name || force_push?
+ merge_request.reload_diff
+ else
+ mr_commit_ids = merge_request.commits.map(&:id)
+ push_commit_ids = @commits.map(&:id)
+ matches = mr_commit_ids & push_commit_ids
+ merge_request.reload_diff if matches.any?
+ end
+ end
end
end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 723cc0e6834..7935fabe2da 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -26,13 +26,16 @@ module Notes
note.note = content
end
- if !only_commands && note.save
+ note.run_after_commit do
# Finish the harder work in the background
- NewNoteWorker.perform_in(2.seconds, note.id, params)
+ NewNoteWorker.perform_async(note.id)
+ end
+
+ if !only_commands && note.save
todo_service.new_note(note, current_user)
end
- if command_params && command_params.any?
+ if command_params.present?
slash_commands_service.execute(command_params, note)
# We must add the error after we call #save because errors are reset
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 72712afc07e..ecdcbf08ee1 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -75,7 +75,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
+ relabeled_resource_email(issue, issue.project, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -118,7 +118,7 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
+ relabeled_resource_email(merge_request, merge_request.target_project, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
@@ -205,7 +205,7 @@ class NotificationService
recipients = reject_muted_users(recipients, note.project)
- recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = add_subscribed_users(recipients, note.project, note.noteable)
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
@@ -312,6 +312,22 @@ class NotificationService
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
+ def pipeline_finished(pipeline, recipients = nil)
+ email_template = "pipeline_#{pipeline.status}_email"
+
+ return unless mailer.respond_to?(email_template)
+
+ recipients ||= build_recipients(
+ pipeline,
+ pipeline.project,
+ nil, # The acting user, who won't be added to recipients
+ action: pipeline.status).map(&:notification_email)
+
+ if recipients.any?
+ mailer.public_send(email_template, pipeline, recipients).deliver_later
+ end
+ end
+
protected
# Get project/group users with CUSTOM notification level
@@ -377,7 +393,7 @@ class NotificationService
)
end
- # Build a list of users based on project notifcation settings
+ # Build a list of users based on project notification settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
users = notification_settings_for(project, :watch)
@@ -475,26 +491,31 @@ class NotificationService
end
def reject_users_without_access(recipients, target)
- return recipients unless target.is_a?(Issuable)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
- ability = :"read_#{target.to_ability_name}"
+ return recipients unless ability
recipients.select do |user|
user.can?(ability, target)
end
end
- def add_subscribed_users(recipients, target)
+ def add_subscribed_users(recipients, project, target)
return recipients unless target.respond_to? :subscribers
- recipients + target.subscribers
+ recipients + target.subscribers(project)
end
- def add_labels_subscribers(recipients, target, labels: nil)
+ def add_labels_subscribers(recipients, project, target, labels: nil)
return recipients unless target.respond_to? :labels
(labels || target.labels).each do |label|
- recipients += label.subscribers
+ recipients += label.subscribers(project)
end
recipients
@@ -550,8 +571,8 @@ class NotificationService
end
end
- def relabeled_resource_email(target, labels, current_user, method)
- recipients = build_relabeled_recipients(target, current_user, labels: labels)
+ def relabeled_resource_email(target, project, labels, current_user, method)
+ recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -587,10 +608,10 @@ class NotificationService
end
recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, target)
+ recipients = add_subscribed_users(recipients, project, target)
if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, target)
+ recipients = add_labels_subscribers(recipients, project, target)
end
recipients = reject_unsubscribed_users(recipients, target)
@@ -601,8 +622,8 @@ class NotificationService
recipients.uniq
end
- def build_relabeled_recipients(target, current_user, labels:)
- recipients = add_labels_subscribers([], target, labels: labels)
+ def build_relabeled_recipients(target, project, current_user, labels:)
+ recipients = add_labels_subscribers([], project, target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user)
@@ -624,6 +645,6 @@ class NotificationService
# Build event key to search on custom notification level
# Check NotificationSetting::EMAIL_EVENTS
def build_custom_key(action, object)
- "#{action}_#{object.class.name.underscore}".to_sym
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 15d7918e7fd..159f46cd465 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -95,7 +95,7 @@ module Projects
unless @project.gitlab_project_import?
@project.create_wiki unless skip_wiki?
- @project.build_missing_services
+ create_services_from_active_templates(@project)
@project.create_labels
end
@@ -106,6 +106,8 @@ module Projects
unless @project.group || @project.gitlab_project_import?
@project.team << [current_user, :master, current_user]
end
+
+ @project.group.refresh_members_authorized_projects if @project.group
end
def skip_wiki?
@@ -135,5 +137,12 @@ module Projects
@project
end
+
+ def create_services_from_active_templates(project)
+ Service.where(template: true, active: true).each do |template|
+ service = Service.build_from_template(project.id, template)
+ service.save!
+ end
+ end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index d38328403c1..6040391fd94 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,7 +1,7 @@
module Projects
class ParticipantsService < BaseService
attr_reader :noteable
-
+
def execute(noteable)
@noteable = noteable
@@ -15,7 +15,8 @@ module Projects
[{
name: noteable.author.name,
- username: noteable.author.username
+ username: noteable.author.username,
+ avatar_url: noteable.author.avatar_url
}]
end
@@ -28,14 +29,14 @@ module Projects
def sorted(users)
users.uniq.to_a.compact.sort_by(&:username).map do |user|
- { username: user.username, name: user.name }
+ { username: user.username, name: user.name, avatar_url: user.avatar_url }
end
end
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
- { username: group.path, name: group.name, count: count }
+ { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 5a81194a5f4..d75c5b1800e 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -193,7 +193,7 @@ module SlashCommands
desc 'Subscribe'
condition do
issuable.persisted? &&
- !issuable.subscribed?(current_user)
+ !issuable.subscribed?(current_user, project)
end
command :subscribe do
@updates[:subscription_event] = 'subscribe'
@@ -202,7 +202,7 @@ module SlashCommands
desc 'Unsubscribe'
condition do
issuable.persisted? &&
- issuable.subscribed?(current_user)
+ issuable.subscribed?(current_user, project)
end
command :unsubscribe do
@updates[:subscription_event] = 'unsubscribe'
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
new file mode 100644
index 00000000000..2469b4f0d7c
--- /dev/null
+++ b/app/services/user_project_access_changed_service.rb
@@ -0,0 +1,9 @@
+class UserProjectAccessChangedService
+ def initialize(user_ids)
+ @user_ids = Array.wrap(user_ids)
+ end
+
+ def execute
+ AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] })
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 450ec322f2c..a236335131a 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -284,6 +284,31 @@
results in fewer but larger UDP packets being sent.
%fieldset
+ %legend Background Jobs
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :sidekiq_throttling_enabled do
+ = f.check_box :sidekiq_throttling_enabled
+ Enable Sidekiq Job Throttling
+ .help-block
+ Limit the amount of resources slow running jobs are assigned.
+ .form-group
+ = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+ .help-block
+ Choose which queues you wish to throttle.
+ .form-group
+ = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+ .help-block
+ The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+ %fieldset
%legend Spam and Anti-bot Protection
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 26a8846b609..5e3f105d41f 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -14,5 +14,5 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
- %ul.content-list.builds-content-list
+ %ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 90798c47d97..1db2150f336 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -87,7 +87,7 @@
%p
GitLab Workhorse
%span.pull-right
- = Gitlab::Workhorse.version
+ = gitlab_workhorse_version
%p
GitLab API
%span.pull-right
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 817910f7ddf..589f4557b52 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -7,7 +7,7 @@
.col-sm-10
= render 'shared/choose_group_avatar_button', f: f
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
.col-sm-offset-2.col-sm-10
diff --git a/app/views/admin/groups/edit.html.haml b/app/views/admin/groups/edit.html.haml
index eb09a6328ed..c2b9807015d 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,4 +1,4 @@
- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
%hr
-= render 'form'
+= render 'form', visibility_level: @group.visibility_level
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index c81ee552ac3..8f9fe96249f 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,4 +1,4 @@
- page_title "New Group"
%h3.page-title New group
%hr
-= render 'form'
+= render 'form', visibility_level: default_group_visibility
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index e247eebc3fc..472d698486b 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -82,15 +82,19 @@
- elsif current_user.todos.any?
.todos-all-done
= render "shared/empty_states/todos_all_done.svg"
- %h4.text-center
- Good job! Looks like you don't have any todos left.
- %p.text-center
- Are you looking for things to do? Take a look at
- = succeed "," do
- = link_to "the opened issues", issues_dashboard_path
- contribute to
- = link_to "merge requests", merge_requests_dashboard_path
- or mention someone in a comment to assign a new todo automatically.
+ - if todos_filter_empty?
+ %h4.text-center
+ = Gitlab.config.gitlab.no_todos_messages.sample
+ %p.text-center
+ Are you looking for things to do? Take a look at
+ = succeed "," do
+ = link_to "the opened issues", issues_dashboard_path
+ contribute to
+ = link_to "merge requests", merge_requests_dashboard_path
+ or mention someone in a comment to assign a new todo automatically.
+ - else
+ %h4.text-center
+ There are no todos to show.
- else
.todos-empty
.todos-empty-hero
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 21b89580818..84e13693dfd 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -5,8 +5,6 @@
%div.form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
- %div.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
.remember-me.checkbox
%label{for: "user_remember_me"}
@@ -14,3 +12,5 @@
%span Remember me
.pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name)
+ %div.submit-container.move-submit-down
+ = f.submit "Sign in", class: "btn btn-save"
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 7c68e3266e5..3133f6de2e8 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
%div.username.form-group
= f.label :username
- = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
+ = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index 1e957f0935f..aec1b31ce62 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -8,3 +8,6 @@
- if signin_enabled?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
+ - if signin_enabled? && signup_enabled?
+ %li
+ = link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 077e8e64e5f..e4b4ea675d2 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,9 +1,6 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
- .timeline-icon
- = link_to user_path(discussion.author) do
- = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
@@ -13,9 +10,7 @@
= icon("chevron-up")
- else
= icon("chevron-down")
-
Toggle discussion
-
= link_to_member(@project, discussion.author, avatar: false)
.inline.discussion-headline-light
@@ -38,8 +33,6 @@
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
- = render "discussions/headline", discussion: discussion
-
.discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
- if discussion.diff_discussion? && discussion.diff_file
= render "discussions/diff_with_notes", discussion: discussion
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 31fdcc5e21b..5c318cd3b8b 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,7 +1,7 @@
- if event.visible_to_user?(current_user)
.event-item{ class: event_row_class(event) }
.event-item-timestamp
- #{time_ago_with_tooltip(event.created_at, skip_js: true)}
+ #{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do
= author_avatar(event, size: 40)
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index 23d438b2aa1..0dfaf743992 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -34,7 +34,7 @@
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
- { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
+ { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
.col-md-6
.form-group
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index b439b40a75a..52ce26a20b1 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,25 +4,23 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.cover-block.groups-cover-block
+.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- .group-info
- .cover-title
- %h1
- @#{@group.path}
- %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, fw: false)
+ %h1.group-title
+ @#{@group.path}
+ %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, fw: false)
- .group-right-buttons.btn-group
- - if current_user
- .pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ - if @group.description.present?
+ .group-home-desc
+ = markdown_field(@group, :description)
- - if @group.description.present?
- .cover-desc.description
- = markdown_field(@group, :description)
+ - if current_user
+ .group-buttons
+ = render 'shared/members/access_request_buttons', source: @group
+ = render 'shared/notifications/button', notification_setting: @notification_setting
%div.groups-header{ class: container_class }
.top-area
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8aefdcb3d9b..a9a0b149049 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -26,5 +26,5 @@
= render "layouts/flash"
= yield :flash_message
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
- .content
+ .content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index d7386105b7d..8e65bd12c56 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -13,7 +13,7 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
- = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7a9859262f7..5456be77aab 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+ %a{ href: "#content-body", tabindex: "1", class: "sr-only gl-accessibility" } Skip to content
%div{ class: "container-fluid" }
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index a0356feef95..2a6d9cda379 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -26,12 +26,12 @@
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
%span
Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
%span
Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ %span.count= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
%span
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 75275afc0f3..c0328fe8842 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -14,7 +14,7 @@
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if can_edit || can_leave
+ - if (can_edit || can_leave) && can_admin_group
%li.divider
- if can_edit
%li
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 6d514f669db..e06301bda14 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -17,6 +17,10 @@
= link_to applications_profile_path, title: 'Applications' do
%span
Applications
+ = nav_link(controller: :chat_names) do
+ = link_to profile_chat_names_path, title: 'Chat' do
+ %span
+ Chat
= nav_link(controller: :personal_access_tokens) do
= link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 0995826775a..38c852f0a3a 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -103,11 +103,11 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
%img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
= @merge_request.to_reference
.commit{style: "color:#5c5c5c;font-weight:300;"}
= @pipeline.git_commit_message.truncate(50)
@@ -134,7 +134,7 @@
%tr.pre-section
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;"}
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
had
= failed.size
@@ -158,7 +158,7 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;"}
= build.stage
%td{align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;"}
- %a{href: pipeline_build_url(@pipeline, build), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_build_url(@pipeline, build), style: "color:#3777b0;text-decoration:none;"}
= build.name
%tr.build-log
%td{colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;"}
@@ -168,10 +168,10 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
%img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
%div
- %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index cf9c1d4d72c..697c8d19257 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -103,11 +103,11 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;"}
%img{height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13"}/
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;"}
- %a{href: commit_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= @pipeline.short_sha
- if @merge_request
in
- %a{href: merge_request_url(@merge_request), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: merge_request_url(@merge_request), style: "color:#3777b0;text-decoration:none;"}
= @merge_request.to_reference
.commit{style: "color:#5c5c5c;font-weight:300;"}
= @pipeline.git_commit_message.truncate(50)
@@ -135,7 +135,7 @@
- build_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages.size
Pipeline
- %a{href: pipeline_url(@pipeline), style: "color:#3084bb;text-decoration:none;"}
+ %a{href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;"}
= "\##{@pipeline.id}"
successfully completed
= "#{build_count} #{'build'.pluralize(build_count)}"
@@ -145,10 +145,10 @@
%td{style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;"}
%img{alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90"}/
%div
- %a{href: profile_notifications_url, style: "color:#3084bb;text-decoration:none;"} Manage all notifications
+ %a{href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;"} Manage all notifications
&middot;
- %a{href: help_url, style: "color:#3084bb;text-decoration:none;"} Help
+ %a{href: help_url, style: "color:#3777b0;text-decoration:none;"} Help
%div
You're receiving this email because of your account on
= succeed "." do
- %a{href: root_url, style: "color:#3084bb;text-decoration:none;"}= Gitlab.config.gitlab.host
+ %a{href: root_url, style: "color:#3777b0;text-decoration:none;"}= Gitlab.config.gitlab.host
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c0c07d65daa..307c5a11206 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -27,9 +27,9 @@
%h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @message.diffs.each_with_index do |diff, i|
+ - @message.diffs.each do |diff|
%li.file-stats
- %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" }
+ %a{href: "#{@message.target_url if @message.disable_diffs?}##{hexdigest(diff.file_path)}" }
- if diff.deleted_file
%span.deleted-file
&minus;
@@ -52,9 +52,10 @@
%h5 The diff was not included because it is too large.
- else
%h4 Changes:
- - diff_files.each_with_index do |diff_file, i|
- %li{id: "diff-#{i}"}
- %a{href: @message.target_url + "#diff-#{i}"}<
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
+ %li{id: file_hash}
+ %a{href: @message.target_url + "##{file_hash}"}<
- if diff_file.deleted_file
%strong<
= diff_file.old_path
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index e2e974ba072..72f658d1b68 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -8,24 +8,36 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- Private Token
+ = incoming_email_token_enabled? ? "Private Tokens" : "Private Token"
%p
- Your private token is used to access application resources without authentication.
- .col-lg-9
- = form_for @user, url: reset_private_token_profile_path, method: :put, html: { class: "private-token" } do |f|
+ Keep
+ = incoming_email_token_enabled? ? "these tokens" : "this token"
+ secret, anyone with access to them can interact with GitLab as if they were you.
+ .col-lg-9.private-tokens-reset
+ .reset-action
%p.cgray
- if current_user.private_token
- = label_tag "token", "Private token", class: "label-light"
- = text_field_tag "token", current_user.private_token, class: "form-control"
+ = label_tag "private-token", "Private token", class: "label-light"
+ = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()"
- else
- %span You don`t have one yet. Click generate to fix it.
- %p.help-block
- It can be used for atom feeds or the API. Keep it secret!
+ %span You don't have one yet. Click generate to fix it.
+ %p.help-block
+ Your private token is used to access the API and Atom feeds without username/password authentication.
.prepend-top-default
- if current_user.private_token
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token"
- else
= f.submit 'Generate', class: "btn btn-default"
+ - if incoming_email_token_enabled?
+ .reset-action
+ %p.cgray
+ = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light'
+ = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()"
+ %p.help-block
+ Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.
+ .prepend-top-default
+ = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token"
+
%hr
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
new file mode 100644
index 00000000000..6b32d377e1a
--- /dev/null
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -0,0 +1,27 @@
+- service = chat_name.service
+- project = service.project
+%tr
+ %td
+ %strong
+ - if can?(current_user, :read_project, project)
+ = link_to project.name_with_namespace, project_path(project)
+ - else
+ .light N/A
+ %td
+ %strong
+ - if can?(current_user, :admin_project, project)
+ = link_to service.title, edit_namespace_project_service_path(project.namespace, project, service)
+ - else
+ = service.title
+ %td
+ = chat_name.team_domain
+ %td
+ = chat_name.chat_name
+ %td
+ - if chat_name.last_used_at
+ time_ago_with_tooltip(chat_name.last_used_at)
+ - else
+ Never
+
+ %td
+ = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger pull-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
new file mode 100644
index 00000000000..20cc636b2da
--- /dev/null
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -0,0 +1,30 @@
+- page_title 'Chat'
+= render 'profiles/head'
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ You can see your Chat accounts.
+
+ .col-lg-9
+ %h5 Active chat names (#{@chat_names.size})
+
+ - if @chat_names.present?
+ .table-responsive
+ %table.table.chat-names
+ %thead
+ %tr
+ %th Project
+ %th Service
+ %th Team domain
+ %th Nickname
+ %th Last used
+ %th
+ %tbody
+ = render @chat_names
+
+ - else
+ .settings-message.text-center
+ You don't have any active chat names.
diff --git a/app/views/profiles/chat_names/new.html.haml b/app/views/profiles/chat_names/new.html.haml
new file mode 100644
index 00000000000..f635acf96e2
--- /dev/null
+++ b/app/views/profiles/chat_names/new.html.haml
@@ -0,0 +1,15 @@
+%h3.page-title Authorization required
+%main{:role => "main"}
+ %p.h4
+ Authorize
+ %strong.text-info= @chat_name_params[:chat_name]
+ to use your account?
+
+ %hr
+ .actions
+ = form_tag profile_chat_names_path, method: :post do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Authorize", class: "btn btn-success wide pull-left"
+ = form_tag deny_profile_chat_names_path, method: :delete do
+ = hidden_field_tag :token, @chat_name_token.token
+ = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index 8e23d51b224..7f530708947 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -8,5 +8,5 @@
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message"
&middot;
-#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} by
+#{time_ago_with_tooltip(commit.committed_date)} by
= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index 539d07d634a..ede01dcc1aa 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,5 +1,4 @@
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
-- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index dfb96305f48..cadfe5a3e30 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -32,7 +32,7 @@
.light
= commit_author_link(commit, avatar: false)
authored
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)}
+ #{time_ago_with_tooltip(commit.committed_date)}
%td.line-numbers
- line_count = blame_group[:lines].count
- (current_line...(current_line + line_count)).each do |i|
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
new file mode 100644
index 00000000000..356bd50f7f3
--- /dev/null
+++ b/app/views/projects/boards/_show.html.haml
@@ -0,0 +1,28 @@
+- @no_container = true
+- @content_class = "issue-boards-content"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('boards/boards_bundle.js')
+ = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
+
+ %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
+ %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
+ %script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
+
+= render "projects/issues/head"
+
+= render 'shared/issuable/filter', type: :boards
+
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %board{ "v-cloak" => true,
+ "v-for" => "list in state.lists",
+ "ref" => "board",
+ ":list" => "list",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ ":key" => "_uid" }
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
index 97eb952eff1..0af40ddf8fe 100644
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -1,5 +1,5 @@
%board-blank-state{ "inline-template" => true,
- "v-if" => "list.id == 'blank'" }
+ "v-if" => 'list.id == "blank"' }
.board-blank-state
%p
Add the following default lists to your Issue Board with one click:
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f7071051efc..a2e5118a9f3 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -1,80 +1,34 @@
-%board{ "inline-template" => true,
- "v-cloak" => true,
- "v-for" => "list in state.lists | orderBy 'position'",
- "v-ref:board" => true,
- ":list" => "list",
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- "track-by" => "_uid" }
- .board{ ":class" => "{ 'is-draggable': !list.preset }",
- ":data-id" => "list.id" }
- .board-inner
- %header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
- %h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
- data: { container: "body", placement: "bottom" } }
- {{ list.title }}
- .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
- %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
- {{ list.issuesSize }}
- - if can?(current_user, :admin_issue, @project)
- %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => "list.type !== 'done'",
- "aria-label" => "Add an issue",
- "title" => "Add an issue",
- data: { placement: "top", container: "body" } }
- = icon("plus")
- - if can?(current_user, :admin_list, @project)
- %board-delete{ "inline-template" => true,
- ":list" => "list",
- "v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
- = icon("trash")
- %board-list{ "inline-template" => true,
- "v-if" => "list.type !== 'blank'",
- ":list" => "list",
- ":issues" => "list.issues",
- ":loading" => "list.loading",
- ":disabled" => "disabled",
- ":show-issue-form.sync" => "showIssueForm",
- ":issue-link-base" => "issueLinkBase" }
- .board-list-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- - if can? current_user, :create_issue, @project
- %board-new-issue{ "inline-template" => true,
+.board{ ":class" => '{ "is-draggable": !list.preset }',
+ ":data-id" => "list.id" }
+ .board-inner
+ %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
+ %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
+ %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
+ data: { container: "body", placement: "bottom" } }
+ {{ list.title }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => 'list.type !== "done"',
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
+ - if can?(current_user, :admin_list, @project)
+ %board-delete{ "inline-template" => true,
":list" => "list",
- ":show-issue-form.sync" => "showIssueForm",
- "v-show" => "list.type !== 'done' && showIssueForm" }
- .card.board-new-issue-form
- %form{ "@submit" => "submit($event)" }
- .flash-container{ "v-if" => "error" }
- .flash-alert
- An error occured. Please try again.
- %label.label-light{ ":for" => "list.id + '-title'" }
- Title
- %input.form-control{ type: "text",
- "v-model" => "title",
- "v-el:input" => true,
- ":id" => "list.id + '-title'" }
- .clearfix.prepend-top-10
- %button.btn.btn-success.pull-left{ type: "submit",
- ":disabled" => "title === ''",
- "v-el:submit-button" => true }
- Submit issue
- %button.btn.btn-default.pull-right{ type: "button",
- "@click" => "cancel" }
- Cancel
- %ul.board-list{ "v-el:list" => true,
- "v-show" => "!loading",
- ":data-board" => "list.id",
- ":class" => "{ 'is-smaller': showIssueForm }" }
- = render "projects/boards/components/card"
- %li.board-list-count.text-center{ "v-if" => "showCount" }
- = icon("spinner spin", "v-show" => "list.loadingMore" )
- %span{ "v-if" => "list.issues.length === list.issuesSize" }
- Showing all issues
- %span{ "v-else" => true }
- Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- - if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ "v-if" => "!list.preset && list.id" }
+ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ = icon("trash")
+ %board-list{ "v-if" => 'list.type !== "blank"',
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ "ref" => "board-list" }
+ - if can?(current_user, :admin_list, @project)
+ = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
new file mode 100644
index 00000000000..d86e0ed8540
--- /dev/null
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -0,0 +1,44 @@
+.board-list-component
+ .board-list-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ "v-if" => 'list.type !== "done" && showIssueForm' }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => 'list.id + "-title"' }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "ref" => "input",
+ ":id" => 'list.id + "-title"' }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => 'title === ""',
+ "ref" => "submit-button" }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
+ %ul.board-list{ "ref" => "list",
+ "v-show" => "!loading",
+ ":data-board" => "list.id",
+ ":class" => '{ "is-smaller": showIssueForm }' }
+ %board-card{ "v-for" => "(issue, index) in orderedIssues",
+ "ref" => "issue",
+ ":index" => "index",
+ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":disabled" => "disabled",
+ "key" => "id" }
+ %li.board-list-count.text-center{ "v-if" => "showCount" }
+ = icon("spinner spin", "v-show" => "list.loadingMore" )
+ %span{ "v-if" => "list.issues.length === list.issuesSize" }
+ Showing all issues
+ %span{ "v-else" => true }
+ Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index 8fce702314c..72b31b8cdae 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -1,36 +1,26 @@
-%board-card{ "inline-template" => true,
- "v-for" => "issue in issues | orderBy 'priority'",
- "v-ref:issue" => true,
- ":index" => "$index",
- ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":disabled" => "disabled",
- "track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
- ":index" => "index",
- "@mousedown" => "mouseDown",
- "@mouseMove" => "mouseMove",
- "@mouseup" => "showIssue($event)" }
- %h4.card-title
- = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
- %a{ ":href" => "issueLinkBase + '/' + issue.id",
- ":title" => "issue.title" }
- {{ issue.title }}
- .card-footer
- %span.card-number{ "v-if" => "issue.id" }
- = precede '#' do
- {{ issue.id }}
- %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
- ":title" => "'Assigned to ' + issue.assignee.name",
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
- %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
- type: "button",
- "v-if" => "(!list.label || label.id !== list.label.id)",
- "@click" => "filterByLabel(label, $event)",
- ":style" => "{ backgroundColor: label.color, color: label.textColor }",
- ":title" => "label.description",
- data: { container: 'body' } }
- {{ label.title }}
+%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
+ ":index" => "index",
+ "@mousedown" => "mouseDown",
+ "@mouseup" => "showIssue($event)" }
+ %h4.card-title
+ = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
+ %a{ ":href" => 'issueLinkBase + "/" + issue.id',
+ ":title" => "issue.title" }
+ {{ issue.title }}
+ .card-footer
+ %span.card-number{ "v-if" => "issue.id" }
+ = precede '#' do
+ {{ issue.id }}
+ %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
+ ":title" => '"Assigned to " + issue.assignee.name',
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
+ %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
+ type: "button",
+ "v-if" => "(!list.label || label.id !== list.label.id)",
+ "@click" => "filterByLabel(label, $event)",
+ ":style" => "{ backgroundColor: label.color, color: label.textColor }",
+ ":title" => "label.description",
+ data: { container: 'body' } }
+ {{ label.title }}
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index f0c0c6953e0..2125c3387c4 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -1,5 +1,5 @@
%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+ ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
.block.issuable-sidebar-header
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
index 604e13858d1..8fe1b832071 100644
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -1,8 +1,8 @@
.block.assignee
.title.hide-collapsed
Assignee
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.hide-collapsed
%span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index c7da1d0d4ac..1a3b88e28c5 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -1,8 +1,8 @@
.block.due_date
.title
Due date
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
.value-content
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index ce68e5e1998..0f0a84c156d 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -1,8 +1,8 @@
.block.labels
.title
Labels
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value.issuable-show-labels
%span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 3cd20d1c0f7..008d1186478 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -1,8 +1,8 @@
.block.milestone
.title
Milestone
- = icon("spinner spin", class: "block-loading")
- if can?(current_user, :admin_issue, @project)
+ = icon("spinner spin", class: "block-loading")
= link_to "Edit", "#", class: "edit-link pull-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 29c9a43a0c1..2a5b8b1441e 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -1,18 +1 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('boards/boards_bundle.js')
- = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
-
-= render "projects/issues/head"
-
-= render 'shared/issuable/filter', type: :boards
-
-#board-app.boards-app{ "v-cloak" => true, data: board_data }
- .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
- = render "projects/boards/components/sidebar"
+= render "show"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 84f38575e84..2246316b540 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -26,6 +26,8 @@
= sort_title_oldest_updated
- if can? current_user, :push_code, @project
+ = link_to namespace_project_merged_branches_path(@project.namespace, @project), class: 'btn btn-inverted btn-remove has-tooltip', title: "Delete all branches that are merged into '#{@project.repository.root_ref}'", method: :delete, data: { confirm: "Deleting the merged branches cannot be undone. Are you sure?", container: 'body' } do
+ Delete merged branches
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
New branch
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 3f2ce7377fd..f6aa20c4579 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -3,6 +3,9 @@
= ci_status_with_icon(@build.status)
Build
%strong ##{@build.id}
+ in pipeline
+ = link_to pipeline_path(@build.pipeline) do
+ %strong ##{@build.pipeline.id}
for commit
= link_to ci_status_path(@build.pipeline) do
%strong= @build.pipeline.short_sha
@@ -14,6 +17,6 @@
= render "user"
= time_ago_with_tooltip(@build.created_at)
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 36294c89fa8..028664f5bba 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -10,6 +10,7 @@
%tr
%th Status
%th Build
+ %th Pipeline
- if admin
%th Project
%th Runner
@@ -19,6 +20,6 @@
%th Coverage
%th
- = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
+ = render partial: "projects/ci/builds/build", collection: builds, as: :build, locals: { commit_sha: true, ref: true, pipeline_link: true, stage: true, allow_retry: true, coverage: admin || project.build_coverage_enabled?, admin: admin }
= paginate builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b5e8b0bf6eb..d8cbfd7173a 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
- trace_with_state = @build.trace_with_state
-- header_title project_title(@project, "Builds", project_builds_path(@project))
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
@@ -27,33 +26,52 @@
= link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
+ - if @build.starts_environment?
+ .prepend-top-default
+ .environment-information
+ - if @build.outdated_deployment?
+ = ci_icon_for_status('success_with_warnings')
+ - else
+ = ci_icon_for_status(@build.status)
+
+ - environment = environment_for_build(@build.project, @build)
+ - if @build.success? && @build.last_deployment.present?
+ - if @build.last_deployment.last?
+ This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}.
+ - else
+ This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
+ - if environment.last_deployment
+ View the most recent deployment #{deployment_link(environment.last_deployment)}.
+ - elsif @build.complete? && !@build.success?
+ The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed.
+ - else
+ This build is creating a deployment to #{environment_link_for_build(@build.project, @build)}
+ - if environment.last_deployment
+ and will overwrite the
+ = link_to 'latest deployment', deployment_link(environment.last_deployment)
+
.prepend-top-default
- - if @build.active?
- .autoscroll-container
- %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
- 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)}
- else
#js-build-scroll.scroll-controls
- = link_to '#build-trace', class: 'btn' do
- %i.fa.fa-angle-up
- = link_to '#down-build-trace', class: 'btn' do
- %i.fa.fa-angle-down
+ .scroll-step
+ = link_to '#build-trace', class: 'btn' do
+ %i.fa.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.fa-angle-down
+ - if @build.active?
+ .autoscroll-container
+ %button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
+ Enable autoscroll
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
- #down-build-trace
+ #down-build-trace
= render "sidebar"
- :javascript
- new Build({
- page_url: "#{namespace_project_build_url(@project.namespace, @project, @build)}",
- build_url: "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}",
- build_status: "#{@build.status}",
- build_stage: "#{@build.stage}",
- state1: "#{trace_with_state[:state]}"
- })
+.js-build-options{ data: javascript_build_options }
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 7e83a88913a..7b995bd8735 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,5 +1,5 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span{class: 'hidden-xs hidden-sm download-button'}
+ %span{class: 'download-button'}
.dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 6cd9b98a706..d3ccebbe290 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,5 +1,5 @@
- if current_user
- .dropdown.inline.project-dropdown
+ .dropdown.inline
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
= icon("caret-down")
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 94632056b15..8d9c15d0dc6 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -2,6 +2,7 @@
- ref = local_assigns.fetch(:ref, nil)
- commit_sha = local_assigns.fetch(:commit_sha, nil)
- retried = local_assigns.fetch(:retried, false)
+- pipeline_link = local_assigns.fetch(:pipeline_link, false)
- stage = local_assigns.fetch(:stage, false)
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
@@ -51,6 +52,16 @@
- if build.manual?
%span.label.label-info manual
+ - if pipeline_link
+ %td
+ = link_to pipeline_path(build.pipeline) do
+ %span.pipeline-id ##{build.pipeline.id}
+ %span by
+ - if build.pipeline.user
+ = user_avatar(user: build.pipeline.user, size: 20)
+ - else
+ %span.monospace API
+
- if admin
%td
- if build.project
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 1f748d73d06..2a2d24be736 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -59,7 +59,7 @@
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
- #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
+ #{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
index 6bb900e3fc1..3a3d750439f 100644
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -8,8 +8,8 @@
- if stage
&nbsp;
= stage.titleize
- = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
- %tr
- %td{colspan: 10}
- &nbsp;
+ = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, allow_retry: true
+ = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, pipeline_link: false, retried: true
+%tr
+ %td{colspan: 10}
+ &nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 0ebc38d16cf..503cbd13b5e 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,5 +1,5 @@
-.commit-info-row.commit-info-row-header
- .commit-meta
+.page-content-header
+ .header-main-content
%strong Commit
%strong.monospace.js-details-short= @commit.short_id
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
@@ -19,7 +19,8 @@
%strong
= commit_committer_link(@commit, avatar: true, size: 24)
#{time_ago_with_tooltip(@commit.committed_date)}
- .commit-action-buttons
+
+ .header-action-buttons
- if defined?(@notes_count) && @notes_count > 0
%span.btn.disabled.btn-grouped.hidden-xs.append-right-10
= icon('comment')
@@ -55,8 +56,8 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
-.commit-info-widget
- .widget-row.branch-info
+.info-well
+ .well-segment.branch-info
.icon-container.commit-icon
= custom_icon("icon_commit")
%span.cgray= pluralize(@commit.parents.count, "parent")
@@ -66,7 +67,7 @@
%i.fa.fa-spinner.fa-spin
- if @commit.status
- .widget-row.pipeline-info
+ .well-segment.pipeline-info
.icon-container
= ci_icon_for_status(@commit.status)
Pipeline
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index d6916fb7f1a..1174158eb65 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,10 +1,6 @@
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 9f80a974d64..12096941209 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -6,7 +6,7 @@
- note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
-- cache_key.push(commit.status) if commit.status
+- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -36,7 +36,6 @@
%pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
- .commit-row-info
- = commit_author_link(commit, avatar: false, size: 24)
- authored
- #{time_ago_with_tooltip(commit.committed_date)}
+ = commit_author_link(commit, avatar: false, size: 24)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index 779c8ea0104..6120b2191dd 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -9,7 +9,7 @@
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
+ - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand
@@ -25,7 +25,7 @@
- elsif diff_file.renamed_file
.nothing-here-block File moved
- elsif blob.image?
- - old_blob = diff_file.old_blob(diff_commit)
+ - old_blob = diff_file.old_blob(diff_file.old_content_commit || @base_commit)
= render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob
- else
.nothing-here-block No preview for this file type
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 067cf595da3..ab4a2dc36e5 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -22,11 +22,12 @@
= render 'projects/diffs/warning', diff_files: diff_files
.files{ data: { can_create_note: can_create_note } }
- - diff_files.each_with_index do |diff_file, index|
+ - diff_files.each_with_index do |diff_file|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
+ - file_hash = hexdigest(diff_file.file_path)
- = render 'projects/diffs/file', index: index, project: diffs.project,
+ = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
diff_file: diff_file, diff_commit: diff_commit, blob: blob
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 8f4f9ad4a80..120ba9ffcd2 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,6 +1,6 @@
-.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)}
+.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)}"}
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule?
.file-actions.hidden-xs
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 7042e9f1fc9..a3e4b5b777e 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -25,9 +25,9 @@
%a{href: "##{line_code}", data: { linenumber: link_text }}
%td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }<
- if email
- %pre= diff_line_content(line.text, type)
+ %pre= diff_line_content(line.text)
- else
- = diff_line_content(line.text, type)
+ = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index e751dabdf99..66d6254aa1e 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -9,28 +9,29 @@
%strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
%ul
- - diff_files.each_with_index do |diff_file, i|
+ - diff_files.each do |diff_file|
+ - file_hash = hexdigest(diff_file.file_path)
%li
- if diff_file.deleted_file
%span.deleted-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-minus
= diff_file.old_path
- elsif diff_file.renamed_file
%span.renamed-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-minus
= diff_file.old_path
&rarr;
= diff_file.new_path
- elsif diff_file.new_file
%span.new-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-plus
= diff_file.new_path
- else
%span.edit-file
- %a{href: "#diff-#{i}"}
+ %a{href: "##{file_hash}"}
%i.fa.fa-adjust
= diff_file.new_path
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
deleted file mode 100644
index b75d5df4150..00000000000
--- a/app/views/projects/environments/_environment.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-- last_deployment = environment.last_deployment
-
-%tr.environment
- %td
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
-
- %td.deployment-column
- - if last_deployment
- %span ##{last_deployment.iid}
- - if last_deployment.user
- by
- = user_avatar(user: last_deployment.user, size: 20)
-
- %td
- - if last_deployment && last_deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
- = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
-
- %td
- - if last_deployment
- = render 'projects/deployments/commit', deployment: last_deployment
- - else
- %p.commit-title
- No deployments yet
-
- %td
- - if last_deployment
- #{time_ago_with_tooltip(last_deployment.created_at)}
-
- %td.hidden-xs
- .pull-right
- = render 'projects/environments/external_url', environment: environment
- = render 'projects/deployments/actions', deployment: last_deployment
- = render 'projects/environments/stop', environment: environment
- = render 'projects/deployments/rollback', deployment: last_deployment
diff --git a/app/views/projects/environments/_header_title.html.haml b/app/views/projects/environments/_header_title.html.haml
deleted file mode 100644
index e056fccad5d..00000000000
--- a/app/views/projects/environments/_header_title.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-- header_title project_title(@project, "Environments", project_environments_path(@project))
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 8f555afcf11..a9235d6af35 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -2,47 +2,19 @@
- page_title "Environments"
= render "projects/pipelines/head"
-%div{ class: container_class }
- .top-area
- %ul.nav-links
- %li{class: ('active' if @scope.nil?)}
- = link_to project_environments_path(@project) do
- Available
- %span.badge.js-available-environments-count
- = number_with_delimiter(@all_environments.available.count)
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag("environments/environments_bundle.js")
+.commit-icon-svg.hidden
+ = custom_icon("icon_commit")
+.play-icon-svg.hidden
+ = custom_icon("icon_play")
- %li{class: ('active' if @scope == 'stopped')}
- = link_to project_environments_path(@project, scope: :stopped) do
- Stopped
- %span.badge.js-stopped-environments-count
- = number_with_delimiter(@all_environments.stopped.count)
-
- - if can?(current_user, :create_environment, @project) && !@all_environments.blank?
- .nav-controls
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
-
- .environments-container
- - if @all_environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.ci-table.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Build
- %th Commit
- %th
- %th.hidden-xs
- = render @environments
+#environments-list-view{ data: { environments_data: environments_list_data,
+ "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
+ "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
+ "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
+ "project-environments-path" => project_environments_path(@project),
+ "project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
+ "new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
+ "help-page-path" => help_page_path("ci/environments"),
+ "css-class" => container_class}}
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 80fe6be49b0..0b99e9f8756 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -15,6 +15,16 @@
- if defined?(retried) && retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Status was retried.')
+ - if defined?(pipeline_link) && pipeline_link
+ %td
+ = link_to pipeline_path(generic_commit_status.pipeline) do
+ %span.pipeline-id ##{generic_commit_status.pipeline.id}
+ %span by
+ - if generic_commit_status.pipeline.user
+ = user_avatar(user: generic_commit_status.pipeline.user, size: 20)
+ - else
+ %span.monospace API
+
- if defined?(commit_sha) && commit_sha
%td
= link_to generic_commit_status.short_sha, namespace_project_commit_path(generic_commit_status.project.namespace, generic_commit_status.project, generic_commit_status.sha), class: "monospace"
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 4d8ee562e6a..c52b3860636 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Import in progress"
+- page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader
.center
%h2
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
index 72669372497..d2038a2be68 100644
--- a/app/views/projects/issues/_issue_by_email.html.haml
+++ b/app/views/projects/issues/_issue_by_email.html.haml
@@ -12,16 +12,23 @@
Create new issue by email
.modal-body
%p
- Write an email to the below email address. (This is a private email address, so keep it secret.)
+ You can create a new issue inside this project by sending an email to the following email address:
.email-modal-input-group.input-group
= text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
= clipboard_button(clipboard_target: '#issue_email')
%p
- Send an email to this address to create an issue.
- %p
- Use the subject line as the title of your issue.
+ The subject will be used as the title of the new issue, and the message will be the description.
+
+ = link_to 'Slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
+ and styling with
+ = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+ are supported.
+
%p
- Use the message as the body of your issue (feel free to include some nice
- = succeed ")." do
- = link_to "Markdown", help_page_path('markdown', 'markdown')
+ This is a private email address, generated just for you.
+
+ Anyone who gets ahold of it can create issues as if they were you.
+ You should
+ = link_to 'reset it', new_issue_address_namespace_project_path(@project.namespace, @project), class: 'incoming-email-token-reset'
+ if that ever happens.
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 31d3ec23276..747bfa554cb 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -19,11 +19,17 @@
in
- project = merge_request.target_project
= link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
- %span.merge-request-status.prepend-left-10
- - if merge_request.merged?
- MERGED
- - elsif merge_request.closed?
- CLOSED
+
+ - if merge_request.merged?
+ %span.merge-request-status.prepend-left-10.merged
+ Merged
+ - elsif merge_request.closed?
+ %span.merge-request-status.prepend-left-10.closed
+ Closed
+ - else
+ %span.merge-request-status.prepend-left-10.open
+ Open
+
- if @closed_by_merge_requests.present?
%li
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 12408068834..9ffcc48eb80 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -54,15 +54,18 @@
= link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
= icon('code-fork')
= merge_request.target_branch
+
- if merge_request.milestone
&nbsp;
= link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
= icon('clock-o')
= merge_request.milestone.title
+
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request)
+
- if merge_request.tasks?
&nbsp;
%span.task-status
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f57abe73977..a497f418c7c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -74,14 +74,15 @@
%span.badge= @merge_request.diff_size
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
- = render "discussions/jump_to_next"
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ = render "shared/icons/icon_status_success.svg"
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index d9f74d2cbfb..16789f68f70 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -30,11 +30,8 @@
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
- = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
+ %parallel-conflict-lines{ ":file" => "file" }
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
-
--# Components
-= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
index f094df7fcaa..d35c7bee163 100644
--- a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
+++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml
@@ -5,11 +5,10 @@
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.old_line}}
- %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
+ %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{{line.richText}}}
+ %strong{"v-html" => "line.richText"}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
deleted file mode 100644
index 5690bf7419c..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
- %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
- %strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
- {{line.buttonTitle}}
- %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{line.lineNumber}}
- %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
- {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
deleted file mode 100644
index a8ecdf59393..00000000000
--- a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
- %table
- %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
- %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 29df1bab04e..d8951e69242 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -17,5 +17,6 @@
= check_box_tag :filter_ref, 1, @options[:filter_ref]
%span Begin with the selected commit
- .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
- = spinner nil, true
+ - if @commit
+ .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
+ = spinner nil, true
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 932603f03b0..0788924d44a 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -90,7 +90,8 @@
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to "(?)", help_page_path("public_access/public_access")
- = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
+
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index ab719e38904..89ae64554c0 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -14,6 +14,9 @@
= note.author.to_reference
- unless note.system
commented
+ - if note.system
+ %span{class: 'system-note-message'}
+ = h(note.note_html.downcase.html_safe)
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
@@ -32,7 +35,7 @@
"resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
- "v-ref:note_#{note.id}" => true }
+ "ref" => "note_#{note.id}" }
.note-action-button
= icon("spin spinner", "v-show" => "loading")
@@ -43,7 +46,7 @@
"@click" => "resolve",
":title" => "buttonText",
"v-show" => "!loading",
- "v-el:button" => true }
+ ":ref" => "'button'" }
= render "shared/icons/icon_status_success.svg"
@@ -67,7 +70,9 @@
= render 'projects/notes/edit_form', note: note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
-
+ - if note.system
+ .system-note-commit-list-toggler
+ Toggle commit list
- if note.attachment.url
.note-attachment
- if note.attachment.image?
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index d288efc546f..a0de125d765 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,39 +1,49 @@
-%p
-.commit-info-row
- Pipeline
- = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @pipeline.id), class: "monospace"
- with
- = pluralize @pipeline.statuses.count(:id), "build"
- - if @pipeline.ref
- for
- = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
-
- .pull-right
+.page-content-header
+ .header-main-content
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
= ci_icon_for_status(@pipeline.status)
= ci_label_for_status(@pipeline.status)
+ %strong Pipeline ##{@commit.pipelines.last.id}
+ triggered #{time_ago_with_tooltip(@commit.authored_date)} by
+ = author_avatar(@commit, size: 24)
+ = commit_author_link(@commit)
+ .header-action-buttons
+ - if can?(current_user, :update_pipeline, @pipeline.project)
+ - if @pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post
+ - if @pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit
- .commit-info-row
- %span.light Authored by
- %strong
- = commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
-
-.commit-info-row
- %span.light Commit
- = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace"
- = clipboard_button(clipboard_text: @pipeline.sha)
-
-- if @commit
- .commit-box.content-block
+ .commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
+
+.info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ .icon-container
+ = ci_icon_for_status(@commit.status)
+ = pluralize @pipeline.statuses.count(:id), "build"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @pipeline.sha, namespace_project_commit_path(@project.namespace, @project, @pipeline.sha), class: "monospace commit-hash-full"
+ = clipboard_button(clipboard_text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
new file mode 100644
index 00000000000..718314701f9
--- /dev/null
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -0,0 +1,51 @@
+.tabs-holder
+ %ul.nav-links.no-top.no-bottom
+ %li.active
+ = link_to "Pipeline", "#js-tab-pipeline", data: { target: '#js-tab-pipeline', action: 'pipeline', toggle: 'tab' }, class: 'pipeline-tab'
+ %li
+ = link_to "#js-tab-builds", data: { target: '#js-tab-builds', action: 'build', toggle: 'tab' }, class: 'builds-tab' do
+ Builds
+ %span.badge= pipeline.statuses.count
+
+.tab-content
+ #js-tab-pipeline.tab-pane.active
+ .build-content.middle-block.pipeline-graph
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ = render "projects/commit/pipeline_stage", statuses: statuses
+
+ #js-tab-builds.tab-pane
+ - if pipeline.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - pipeline.yaml_errors.split(",").each do |error|
+ %li= error
+ You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
+
+ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+ .table-holder.pipeline-holder
+ %table.table.ci-table.pipeline
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Name
+ %th
+ - if pipeline.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ - pipeline.statuses.relevant.stages.each do |stage|
+ = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 688535ad764..8c6652a5f90 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -3,9 +3,7 @@
= render "projects/pipelines/head"
%div{ class: container_class }
- .prepend-top-default
- - if @commit
- = render "projects/pipelines/info"
- %div.block-connector
+ - if @commit
+ = render "projects/pipelines/info"
- = render "projects/commit/pipeline", pipeline: @pipeline
+ = render "projects/pipelines/with_tabs", pipeline: @pipeline
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 1141168f037..d07bb661615 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -14,5 +14,8 @@
// Load more commit logs for each file in tree
// if we still on the same page
var url = "#{escape_javascript(@more_log_url)}";
- ajaxGet(url);
+ gl.utils.ajaxGet(url);
}
+
+:plain
+ gl.utils.localTimeAgo($('.js-timeago', 'table.table_#{@hex_path} tbody'));
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index 4a33a5bc6f6..66fd3029dc9 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -28,5 +28,6 @@
%td.hidden-xs
= service.description
%td.light
- = time_ago_in_words service.updated_at
- ago
+ - if service.updated_at.present?
+ = time_ago_in_words service.updated_at
+ ago
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4de95036eef..c50093cf47c 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -66,8 +66,8 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
- %li.project-repo-buttons-right
- .project-repo-buttons.project-right-buttons
+ %li.project-repo-buttons.right
+ .project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
@@ -76,7 +76,8 @@
= render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
- = render 'shared/notifications/button', notification_setting: @notification_setting
+ .pull-right
+ = render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index f6e0b0a7c8a..6e5dd1b196d 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -76,6 +76,16 @@
script:
- "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
%h5.prepend-top-default
+ Use webhook
+
+ %p.light
+ Add the following webhook to another project for Push and Tag push events.
+ The project will be rebuilt at the corresponding event.
+
+ %pre
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
+ %h5.prepend-top-default
Pass build variables
%p.light
@@ -83,10 +93,18 @@
%code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
- %pre.append-bottom-0
+ With cURL:
+
+ %pre
:plain
curl -X POST \
-F token=TOKEN \
-F "ref=REF_NAME" \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
#{builds_trigger_url(@project.id)}
+ %p.light
+ With webhook:
+
+ %pre.append-bottom-0
+ :plain
+ #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN&variables[RUN_NIGHTLY_BUILD]=true
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 6f0a0ea36ec..9e8adc82583 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,11 +1,13 @@
-- blob = parse_search_result(blob)
+- file_name, blob = blob
.blob-result
.file-holder
.file-title
- - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(blob.ref, blob.filename))
+ - ref = @search_results.repository_ref
+ - blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
%i.fa.fa-file
%strong
- = blob.filename
- .file-content.code.term
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
+ = file_name
+ - if blob
+ .file-content.code.term
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
index 5b2d83d6b92..f34eaf89027 100644
--- a/app/views/search/results/_commit.html.haml
+++ b/app/views/search/results/_commit.html.haml
@@ -1 +1 @@
-= render 'projects/commits/commit', project: @project, commit: commit
+= render 'projects/commits/commit', project: @project, commit: commit, ref: nil
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 6ccdef0df46..db324d8868e 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,7 @@
- label_css_id = dom_id(label)
- open_issues_count = label.open_issues_count(current_user)
- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
%li{id: label_css_id, data: { id: label.id } }
@@ -18,10 +19,19 @@
%li
= link_to_label(label, subject: subject) do
= pluralize open_issues_count, 'open issue'
- - if current_user
- %li.label-subscription{ data: toggle_subscription_data(label) }
- %a.js-subscribe-button.label-subscribe-button.subscription-status{ role: "button", href: "#", data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span= label_subscription_toggle_button_text(label)
+ - if current_user && defined?(@project)
+ %li.label-subscription
+ - if label.is_a?(ProjectLabel)
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ - else
+ %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span Subscribe at project level
+ %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ %span Subscribe at group level
+
- if can?(current_user, :admin_label, label)
%li
= link_to 'Edit', edit_label_path(label)
@@ -34,12 +44,27 @@
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- - if current_user
- .label-subscription.inline{ data: toggle_subscription_data(label) }
- %button.js-subscribe-button.label-subscribe-button.btn.btn-transparent.btn-action.subscription-status{ type: "button", title: label_subscription_toggle_button_text(label), data: { toggle: "tooltip", status: label_subscription_status(label) } }
- %span.sr-only= label_subscription_toggle_button_text(label)
- = icon('eye', class: 'label-subscribe-button-icon', disabled: label.is_a?(GroupLabel))
- = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - if current_user && defined?(@project)
+ .label-subscription.inline
+ - if label.is_a?(ProjectLabel)
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', title: label_subscription_toggle_button_text(label, @project), data: { toggle: 'tooltip', status: status, url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ %span= label_subscription_toggle_button_text(label, @project)
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+ - else
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default.btn-action{ type: 'button', class: ('hidden' if status.unsubscribed?), title: 'Unsubscribe', data: { toggle: 'tooltip', url: group_label_unsubscribe_path(label, @project) } }
+ %span Unsubscribe
+ = icon('spinner spin', class: 'label-subscribe-button-loading')
+
+ .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span Subscribe
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ %li
+ %a.js-subscribe-button{ data: { url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label) } }
+ Project level
+ %a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
+ Group level
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
@@ -49,6 +74,10 @@
%span.sr-only Delete
= icon('trash-o')
- - if current_user && label.is_a?(ProjectLabel)
- :javascript
- new Subscription('##{dom_id(label)} .label-subscription');
+ - if current_user && defined?(@project)
+ - if label.is_a?(ProjectLabel)
+ :javascript
+ new gl.ProjectLabelSubscription('##{dom_id(label)} .label-subscription');
+ - else
+ :javascript
+ new gl.GroupLabelSubscription('##{dom_id(label)} .label-subscription');
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 5254d265918..601ef51737a 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -10,26 +10,27 @@
.col-sm-10
= form.check_box :active
-.form-group
- = form.label :url, "Trigger", class: 'control-label'
-
- .col-sm-10
- - @service.supported_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'pull-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
- %strong
- = event.humanize
-
- - field = @service.event_field(event)
-
- - if field
- %p
- = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
-
- %p.light
- = service_event_description(event)
+- if @service.supported_events.present?
+ .form-group
+ = form.label :url, "Trigger", class: 'control-label'
+
+ .col-sm-10
+ - @service.supported_events.each do |event|
+ %div
+ = form.check_box service_event_field_name(event), class: 'pull-left'
+ .prepend-left-20
+ = form.label service_event_field_name(event), class: 'list-label' do
+ %strong
+ = event.humanize
+
+ - field = @service.event_field(event)
+
+ - if field
+ %p
+ = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
+
+ %p.light
+ = service_event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
index 014ca86b61b..e0a2d4282f0 100644
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 8d976952781..9b9ad510444 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,4 @@
+- form = local_assigns.fetch(:f)
- project = @target_project || @project
= form_errors(issuable)
@@ -10,44 +11,17 @@
and make sure your changes will not unintentionally remove theirs
.form-group
- = f.label :title, class: 'control-label'
+ = form.label :title, class: 'control-label'
= render 'shared/issuable/form/template_selector', issuable: issuable
-
- %div{ class: issuable_templates(issuable).any? ? 'col-sm-7 col-lg-8' : 'col-sm-10' }
- = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
- class: 'form-control pad', required: true
-
- - if issuable.is_a?(MergeRequest)
- %p.help-block
- .js-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Remove the
- %code WIP:
- prefix from the title
- to allow this
- %strong Work In Progress
- merge request to be merged when it's ready.
- .js-no-wip-explanation
- %a.js-toggle-wip{href: "", tabindex: -1}
- Start the title with
- %code WIP:
- to prevent a
- %strong Work In Progress
- merge request from being merged before it's ready.
-
- - if can_add_template?(issuable)
- %p.help-block
- Add
- = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
- to help your contributors communicate effectively!
+ = render 'shared/issuable/form/title', issuable: issuable, form: form
.form-group.detail-page-description
- = f.label :description, 'Description', class: 'control-label'
+ = form.label :description, 'Description', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description,
+ = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea',
placeholder: "Write a comment or drag your files here...",
supports_slash_commands: !issuable.persisted?
@@ -59,8 +33,8 @@
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
- = f.label :confidential do
- = f.check_box :confidential
+ = form.label :confidential do
+ = form.check_box :confidential
This issue is confidential and should only be visible to team members with at least Reporter access.
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
@@ -69,32 +43,32 @@
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
.form-group.issue-assignee
- = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- if issuable.assignee_id
- = f.hidden_field :assignee_id
+ = form.hidden_field :assignee_id
= dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
.form-group.issue-milestone
- = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- has_labels = @labels && @labels.any?
- = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
- = f.hidden_field :label_ids, multiple: true, value: ''
+ = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = form.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
.issuable-form-select-holder
= render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false}, dropdown_title: "Select label"
- if has_due_date
.col-lg-6
.form-group
- = f.label :due_date, "Due date", class: "control-label"
+ = form.label :due_date, "Due date", class: "control-label"
.col-sm-10
.issuable-form-select-holder
- = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+ = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
- if issuable.can_move?(current_user)
%hr
@@ -112,15 +86,15 @@
%hr
- if @merge_request.new_record?
.form-group
- = f.label :source_branch, class: 'control-label'
+ = form.label :source_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ = form.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
- = f.label :target_branch, class: 'control-label'
+ = form.label :target_branch, class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
+ = form.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
@@ -129,16 +103,16 @@
.col-sm-10.col-sm-offset-2
.checkbox
= label_tag 'merge_request[force_remove_source_branch]' do
- = hidden_field_tag 'merge_request[force_remove_source_branch]', '0'
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', @merge_request.force_remove_source_branch?
Remove source branch when merge request is accepted.
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.row-content-block{class: (is_footer ? "footer-block" : "middle-block")}
- if issuable.new_record?
- = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
- = f.submit 'Save changes', class: 'btn btn-save'
+ = form.submit 'Save changes', class: 'btn btn-save'
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
@@ -151,8 +125,8 @@
- else
.pull-right
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
- = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+ = link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.human_class_name} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
-= f.hidden_field :lock_version
+= form.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 3bc57d3d2ac..bd66f39fa59 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -9,7 +9,7 @@
&nbsp
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
- %input#new_label_color.default-dropdown-input{ type: "text" }
+ %input#new_label_color.default-dropdown-input{ type: "text", placeholder: "Assign custom color like #FF0000" }
.clearfix
%button.btn.btn-primary.pull-left.js-new-label-btn{ type: "button" }
Create
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 7363ead09ff..f166fac105d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -140,7 +140,7 @@
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- - subscribed = issuable.subscribed?(current_user)
+ - subscribed = issuable.subscribed?(current_user, @project)
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
new file mode 100644
index 00000000000..83efdc7c8f7
--- /dev/null
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -0,0 +1,32 @@
+- issuable = local_assigns.fetch(:issuable)
+- 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'
+
+%div{ class: div_class }
+ = form.text_field :title, required: true, maxlength: 255, autofocus: true,
+ autocomplete: 'off', class: 'form-control pad'
+
+ - if issuable.respond_to?(:work_in_progress?)
+ %p.help-block
+ .js-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Remove the
+ %code WIP:
+ prefix from the title
+ to allow this
+ %strong Work In Progress
+ merge request to be merged when it's ready.
+ .js-no-wip-explanation
+ %a.js-toggle-wip{ href: '', tabindex: -1 }
+ Start the title with
+ %code WIP:
+ to prevent a
+ %strong Work In Progress
+ merge request from being merged before it's ready.
+
+ - if no_issuable_templates && can?(current_user, :push_code, issuable.project)
+ %p.help-block
+ Add
+ = link_to 'description templates', help_page_path('user/project/description_templates'), tabindex: -1
+ to help your contributors communicate effectively!
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index feaa5570c21..1f7df0bcd19 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -1,6 +1,6 @@
- left_align = local_assigns[:left_align]
- if notification_setting
- .dropdown.notification-dropdown.pull-right
+ .dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b704981e3db..a82fc95df84 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -27,5 +27,5 @@
%label{ for: field_id }
= check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
%strong
- = event.to_s.humanize
+ = notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
new file mode 100644
index 00000000000..331727ba9d8
--- /dev/null
+++ b/app/workers/authorized_projects_worker.rb
@@ -0,0 +1,15 @@
+class AuthorizedProjectsWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def self.bulk_perform_async(args_list)
+ Sidekiq::Client.push_bulk('class' => self, 'args' => args_list)
+ end
+
+ def perform(user_id)
+ user = User.find_by(id: user_id)
+ return unless user
+
+ user.refresh_authorized_projects
+ end
+end
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index e0ad5268664..e17add7421f 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -4,15 +4,13 @@ class BuildSuccessWorker
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
- create_deployment(build)
+ create_deployment(build) if build.has_environment?
end
end
private
def create_deployment(build)
- return if build.environment.blank?
-
service = CreateDeploymentService.new(
build.project, build.user,
environment: build.environment,
diff --git a/app/workers/delete_merged_branches_worker.rb b/app/workers/delete_merged_branches_worker.rb
new file mode 100644
index 00000000000..f870da4ecfd
--- /dev/null
+++ b/app/workers/delete_merged_branches_worker.rb
@@ -0,0 +1,20 @@
+class DeleteMergedBranchesWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ def perform(project_id, user_id)
+ begin
+ project = Project.find(project_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ begin
+ DeleteMergedBranchesService.new(project, user).execute
+ rescue Gitlab::Access::AccessDeniedError
+ return
+ end
+ end
+end
diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb
index c3e62bb88c0..66574d0fd01 100644
--- a/app/workers/new_note_worker.rb
+++ b/app/workers/new_note_worker.rb
@@ -2,10 +2,12 @@ class NewNoteWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
- def perform(note_id, note_params)
- note = Note.find(note_id)
-
- NotificationService.new.new_note(note)
- Notes::PostProcessService.new(note).execute
+ def perform(note_id)
+ if note = Note.find_by(id: note_id)
+ NotificationService.new.new_note(note)
+ Notes::PostProcessService.new(note).execute
+ else
+ Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job")
+ end
end
end
diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb
index 34f6ef161fb..070943f1ecc 100644
--- a/app/workers/pipeline_metrics_worker.rb
+++ b/app/workers/pipeline_metrics_worker.rb
@@ -12,11 +12,11 @@ class PipelineMetricsWorker
private
def update_metrics_for_active_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil, pipeline_id: pipeline.id)
end
def update_metrics_for_succeeded_pipeline(pipeline)
- metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at)
+ metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id)
end
def metrics(pipeline)
diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb
new file mode 100644
index 00000000000..cdb860b6675
--- /dev/null
+++ b/app/workers/pipeline_notification_worker.rb
@@ -0,0 +1,12 @@
+class PipelineNotificationWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ def perform(pipeline_id, recipients = nil)
+ pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+
+ return unless pipeline
+
+ NotificationService.new.pipeline_finished(pipeline, recipients)
+ end
+end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
new file mode 100644
index 00000000000..071741fbacd
--- /dev/null
+++ b/app/workers/process_commit_worker.rb
@@ -0,0 +1,67 @@
+# Worker for processing individiual commit messages pushed to a repository.
+#
+# Jobs for this worker are scheduled for every commit that is being pushed. As a
+# result of this the workload of this worker should be kept to a bare minimum.
+# Consider using an extra worker if you need to add any extra (and potentially
+# slow) processing of commits.
+class ProcessCommitWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+
+ # project_id - The ID of the project this commit belongs to.
+ # user_id - The ID of the user that pushed the commit.
+ # commit_sha - The SHA1 of the commit to process.
+ # default - The data was pushed to the default branch.
+ def perform(project_id, user_id, commit_sha, default = false)
+ project = Project.find_by(id: project_id)
+
+ return unless project
+
+ user = User.find_by(id: user_id)
+
+ return unless user
+
+ commit = find_commit(project, commit_sha)
+
+ return unless commit
+
+ author = commit.author || user
+
+ process_commit_message(project, commit, user, author, default)
+
+ update_issue_metrics(commit, author)
+ end
+
+ def process_commit_message(project, commit, user, author, default = false)
+ closed_issues = default ? commit.closes_issues(user) : []
+
+ unless closed_issues.empty?
+ close_issues(project, user, author, commit, closed_issues)
+ end
+
+ commit.create_cross_references!(author, closed_issues)
+ end
+
+ def close_issues(project, user, author, commit, issues)
+ # We don't want to run permission related queries for every single issue,
+ # therefor we use IssueCollection here and skip the authorization check in
+ # Issues::CloseService#execute.
+ IssueCollection.new(issues).updatable_by_user(user).each do |issue|
+ Issues::CloseService.new(project, author).
+ close_issue(issue, commit: commit)
+ end
+ end
+
+ def update_issue_metrics(commit, author)
+ mentioned_issues = commit.all_references(author).issues
+
+ Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+ update_all(first_mentioned_in_commit_at: commit.committed_date)
+ end
+
+ private
+
+ def find_commit(project, sha)
+ project.commit(sha)
+ end
+end
diff --git a/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml
new file mode 100644
index 00000000000..18fb8a6ad45
--- /dev/null
+++ b/changelogs/unreleased/19981-admin-links-new-group-default-visibility.yml
@@ -0,0 +1,4 @@
+---
+title: Make New Group form respect default visibility application setting
+merge_request: 7454
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/21076-deleted-merged-branches.yml b/changelogs/unreleased/21076-deleted-merged-branches.yml
new file mode 100644
index 00000000000..b7fa7f14384
--- /dev/null
+++ b/changelogs/unreleased/21076-deleted-merged-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Add button to delete all merged branches
+merge_request: 6449
+author: Toon Claes
diff --git a/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
new file mode 100644
index 00000000000..95d8fef1099
--- /dev/null
+++ b/changelogs/unreleased/21664-incorrect-workhorse-version-number-displayed.yml
@@ -0,0 +1,4 @@
+---
+title: Use the Gitlab Workhorse HTTP header in the admin dashboard
+merge_request:
+author: Chris Wright
diff --git a/changelogs/unreleased/21992-disable-access-requests-by-default.yml b/changelogs/unreleased/21992-disable-access-requests-by-default.yml
new file mode 100644
index 00000000000..ddcb2169407
--- /dev/null
+++ b/changelogs/unreleased/21992-disable-access-requests-by-default.yml
@@ -0,0 +1,4 @@
+---
+title: Disable "Request Access" functionality by default for new projects and groups
+merge_request: 7425
+author:
diff --git a/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml b/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml
new file mode 100644
index 00000000000..3af746cd92a
--- /dev/null
+++ b/changelogs/unreleased/22307-pipeline-link-in-builds-view.yml
@@ -0,0 +1,4 @@
+---
+title: Add link to build pipeline within individual build pages
+merge_request: 7082
+author:
diff --git a/changelogs/unreleased/22539-display-folders.yml b/changelogs/unreleased/22539-display-folders.yml
new file mode 100644
index 00000000000..d46cdedf7a7
--- /dev/null
+++ b/changelogs/unreleased/22539-display-folders.yml
@@ -0,0 +1,4 @@
+---
+title: Display "folders" for environments
+merge_request: 7015
+author:
diff --git a/changelogs/unreleased/22699-group-permssion-background-migration.yml b/changelogs/unreleased/22699-group-permssion-background-migration.yml
new file mode 100644
index 00000000000..e8c221b6c42
--- /dev/null
+++ b/changelogs/unreleased/22699-group-permssion-background-migration.yml
@@ -0,0 +1,4 @@
+---
+title: Fix project records with invalid visibility_level values
+merge_request: 7391
+author:
diff --git a/changelogs/unreleased/22790-mention-autocomplete-avatar.yml b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml
new file mode 100644
index 00000000000..53068ca5607
--- /dev/null
+++ b/changelogs/unreleased/22790-mention-autocomplete-avatar.yml
@@ -0,0 +1,4 @@
+---
+title: Show avatars in mention dropdown
+merge_request: 6865
+author:
diff --git a/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
new file mode 100644
index 00000000000..7b54d3df56d
--- /dev/null
+++ b/changelogs/unreleased/23036-replace-git-blame-spinach-tests-with-rspec-feature-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Rewrite git blame spinach feature tests to rspec feature tests
+merge_request: 7197
+author: Lisanne Fellinger
diff --git a/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
new file mode 100644
index 00000000000..156f8d779ca
--- /dev/null
+++ b/changelogs/unreleased/23117-search-for-a-filename-in-a-project.yml
@@ -0,0 +1,4 @@
+---
+title: Search for a filename in a project
+merge_request:
+author:
diff --git a/changelogs/unreleased/23223-group-deletion-race-condition.yml b/changelogs/unreleased/23223-group-deletion-race-condition.yml
new file mode 100644
index 00000000000..6f22e85fb4b
--- /dev/null
+++ b/changelogs/unreleased/23223-group-deletion-race-condition.yml
@@ -0,0 +1,4 @@
+---
+title: Fix race condition during group deletion and remove stale records present due to this bug
+merge_request: 7528
+author: Timothy Andrew
diff --git a/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml
new file mode 100644
index 00000000000..59e0d851366
--- /dev/null
+++ b/changelogs/unreleased/23584-triggering-builds-from-webhooks.yml
@@ -0,0 +1,4 @@
+---
+title: Make it possible to trigger builds from webhooks
+merge_request: 7022
+author: Dmitry Poray
diff --git a/changelogs/unreleased/23637-title-bar-pipelines.yml b/changelogs/unreleased/23637-title-bar-pipelines.yml
new file mode 100644
index 00000000000..3d4cf88c54c
--- /dev/null
+++ b/changelogs/unreleased/23637-title-bar-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Redesign pipelines page
+merge_request:
+author:
diff --git a/changelogs/unreleased/23731-add-param-to-user-api.yml b/changelogs/unreleased/23731-add-param-to-user-api.yml
new file mode 100644
index 00000000000..e31029ffb27
--- /dev/null
+++ b/changelogs/unreleased/23731-add-param-to-user-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add query param to filter users by external & blocked type
+merge_request: 7109
+author: Yatish Mehta
diff --git a/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml b/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml
new file mode 100644
index 00000000000..8d4593d4df7
--- /dev/null
+++ b/changelogs/unreleased/23990-project-show-error-when-empty-repo.yml
@@ -0,0 +1,4 @@
+---
+title: fixes 500 error on project show when user is not logged in and project is still empty
+merge_request: 7376
+author:
diff --git a/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml b/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml
new file mode 100644
index 00000000000..33ce18b2141
--- /dev/null
+++ b/changelogs/unreleased/24010-change-anchor-link-to-mr-diff.yml
@@ -0,0 +1,4 @@
+---
+title: Unify anchor link format for MR diff files
+merge_request: 7298
+author: YarNayar
diff --git a/changelogs/unreleased/24010-double-event-trigger.yml b/changelogs/unreleased/24010-double-event-trigger.yml
new file mode 100644
index 00000000000..3c2f20d391f
--- /dev/null
+++ b/changelogs/unreleased/24010-double-event-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Fix double event and ajax request call on MR page
+merge_request: 7298
+author: YarNayar
diff --git a/changelogs/unreleased/24048-dropdown-issue-with-devider.yml b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml
new file mode 100644
index 00000000000..b889da61957
--- /dev/null
+++ b/changelogs/unreleased/24048-dropdown-issue-with-devider.yml
@@ -0,0 +1,4 @@
+---
+title: "[Fix] Extra divider issue in dropdown"
+merge_request: 7398
+author:
diff --git a/changelogs/unreleased/24070-project-margins.yml b/changelogs/unreleased/24070-project-margins.yml
new file mode 100644
index 00000000000..cbc2b0269f0
--- /dev/null
+++ b/changelogs/unreleased/24070-project-margins.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Margins look weird in Project page with pinned sidebar in project stats bar
+merge_request: 7580
+author:
diff --git a/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml b/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml
new file mode 100644
index 00000000000..2c265960d67
--- /dev/null
+++ b/changelogs/unreleased/24072-improve-importing-of-github-pull-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Reduce API calls needed when importing issues and pull requests from GitHub
+merge_request: 7241
+author: Andrew Smith (EspadaV8)
diff --git a/changelogs/unreleased/24107-slack-comment-link.yml b/changelogs/unreleased/24107-slack-comment-link.yml
new file mode 100644
index 00000000000..9c17d6fd825
--- /dev/null
+++ b/changelogs/unreleased/24107-slack-comment-link.yml
@@ -0,0 +1,4 @@
+---
+title: Change slack notification comment link
+merge_request: 7498
+author: Herbert Kagumba
diff --git a/changelogs/unreleased/24255-search-fix.yml b/changelogs/unreleased/24255-search-fix.yml
new file mode 100644
index 00000000000..c0afade9bc8
--- /dev/null
+++ b/changelogs/unreleased/24255-search-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken commits search
+merge_request:
+author:
diff --git a/changelogs/unreleased/24276-usernames-with-dots.yml b/changelogs/unreleased/24276-usernames-with-dots.yml
new file mode 100644
index 00000000000..9aeeb33e9ef
--- /dev/null
+++ b/changelogs/unreleased/24276-usernames-with-dots.yml
@@ -0,0 +1,4 @@
+---
+title: Allow registering users whose username contains dots
+merge_request: 7500
+author: Timothy Andrew
diff --git a/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml
new file mode 100644
index 00000000000..72e7110d1b8
--- /dev/null
+++ b/changelogs/unreleased/24279-issue-merge-request-sidebar-todo-button-style-improvement.yml
@@ -0,0 +1,4 @@
+---
+title: Removed gray button styling from todo buttons in sidebars
+merge_request: 7387
+author:
diff --git a/changelogs/unreleased/24369-remove-additional-padding.yml b/changelogs/unreleased/24369-remove-additional-padding.yml
new file mode 100644
index 00000000000..a6a0b248412
--- /dev/null
+++ b/changelogs/unreleased/24369-remove-additional-padding.yml
@@ -0,0 +1,4 @@
+---
+title: Remove additional padding on right-aligned items in MR widget.
+merge_request: 7411
+author: Didem Acet
diff --git a/changelogs/unreleased/24492-promise-polyfill.yml b/changelogs/unreleased/24492-promise-polyfill.yml
new file mode 100644
index 00000000000..d2fddfd83c6
--- /dev/null
+++ b/changelogs/unreleased/24492-promise-polyfill.yml
@@ -0,0 +1,4 @@
+---
+title: Adds es6-promise Polyfill
+merge_request: 7482
+author:
diff --git a/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml b/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml
new file mode 100644
index 00000000000..a95295c00f3
--- /dev/null
+++ b/changelogs/unreleased/24496-fix-internal-api-project-lookup.yml
@@ -0,0 +1,4 @@
+---
+title: Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths
+merge_request: 7480
+author:
diff --git a/changelogs/unreleased/24627-fix-bad-mr-error-message.yml b/changelogs/unreleased/24627-fix-bad-mr-error-message.yml
new file mode 100644
index 00000000000..d6a9818b2ce
--- /dev/null
+++ b/changelogs/unreleased/24627-fix-bad-mr-error-message.yml
@@ -0,0 +1,4 @@
+---
+title: Fix regression causing bad error message to appear on Merge Request form
+merge_request: 7599
+author: Alex Sanford
diff --git a/changelogs/unreleased/adam-build-missing-services-when-necessary.yml b/changelogs/unreleased/adam-build-missing-services-when-necessary.yml
new file mode 100644
index 00000000000..8b157e31e99
--- /dev/null
+++ b/changelogs/unreleased/adam-build-missing-services-when-necessary.yml
@@ -0,0 +1,4 @@
+---
+title: Defer saving project services to the database if there are no user changes
+merge_request: 6958
+author:
diff --git a/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml
new file mode 100644
index 00000000000..c83558f33d1
--- /dev/null
+++ b/changelogs/unreleased/adam-fix-collapsed-diff-symlink-file-conversion.yml
@@ -0,0 +1,4 @@
+---
+title: Fix expanding a collapsed diff when converting a symlink to a regular file
+merge_request: 6953
+author:
diff --git a/changelogs/unreleased/add-api-label-id.yml b/changelogs/unreleased/add-api-label-id.yml
new file mode 100644
index 00000000000..3af4f5e677d
--- /dev/null
+++ b/changelogs/unreleased/add-api-label-id.yml
@@ -0,0 +1,4 @@
+---
+title: Expose label IDs in API
+merge_request: 7275
+author: Rares Sfirlogea
diff --git a/changelogs/unreleased/add-chat-names.yml b/changelogs/unreleased/add-chat-names.yml
new file mode 100644
index 00000000000..6a1e05783a3
--- /dev/null
+++ b/changelogs/unreleased/add-chat-names.yml
@@ -0,0 +1,4 @@
+---
+title: Allow to connect Chat account with GitLab
+merge_request: 7450
+author:
diff --git a/changelogs/unreleased/always-show-download-button.yml b/changelogs/unreleased/always-show-download-button.yml
new file mode 100644
index 00000000000..3a625834d01
--- /dev/null
+++ b/changelogs/unreleased/always-show-download-button.yml
@@ -0,0 +1,4 @@
+---
+title: Project download buttons always show
+merge_request: 7405
+author: Philip Karpiak
diff --git a/changelogs/unreleased/api-label-priorities.yml b/changelogs/unreleased/api-label-priorities.yml
index 85b6c2761bb..d703f8d32f3 100644
--- a/changelogs/unreleased/api-label-priorities.yml
+++ b/changelogs/unreleased/api-label-priorities.yml
@@ -1,4 +1,4 @@
---
-title: API: Ability to retrieve version information
+title: "API: Ability to retrieve version information"
merge_request: 7286
author: Robert Schilling
diff --git a/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
new file mode 100644
index 00000000000..d132d7e79c3
--- /dev/null
+++ b/changelogs/unreleased/api-return-400-if-post-systemhook-fails.yml
@@ -0,0 +1,4 @@
+---
+title: Return 400 when creating a system hook fails
+merge_request: 7350
+author: Robert Schilling
diff --git a/changelogs/unreleased/assignee-dropdown-autocomplete.yml b/changelogs/unreleased/assignee-dropdown-autocomplete.yml
new file mode 100644
index 00000000000..9d046b726b7
--- /dev/null
+++ b/changelogs/unreleased/assignee-dropdown-autocomplete.yml
@@ -0,0 +1,4 @@
+---
+title: Assignee dropdown now searches author of issue or merge request
+merge_request:
+author:
diff --git a/changelogs/unreleased/bugfix-html-only-mail.yml b/changelogs/unreleased/bugfix-html-only-mail.yml
new file mode 100644
index 00000000000..ea0d4e7396f
--- /dev/null
+++ b/changelogs/unreleased/bugfix-html-only-mail.yml
@@ -0,0 +1,4 @@
+---
+title: Add support for reply-by-email when the email only contains HTML
+merge_request: 7397
+author:
diff --git a/changelogs/unreleased/changelog-update.yml b/changelogs/unreleased/changelog-update.yml
new file mode 100644
index 00000000000..24fa34f4121
--- /dev/null
+++ b/changelogs/unreleased/changelog-update.yml
@@ -0,0 +1,4 @@
+---
+title: Add environment info to builds page
+merge_request:
+author:
diff --git a/changelogs/unreleased/create-pipeline-endpoint.yml b/changelogs/unreleased/create-pipeline-endpoint.yml
new file mode 100644
index 00000000000..c7638b7b7aa
--- /dev/null
+++ b/changelogs/unreleased/create-pipeline-endpoint.yml
@@ -0,0 +1,4 @@
+---
+title: Add api endpoint for creating a pipeline
+merge_request: 7209
+author: Ido Leibovich
diff --git a/changelogs/unreleased/dz-fix-group-name-dot.yml b/changelogs/unreleased/dz-fix-group-name-dot.yml
new file mode 100644
index 00000000000..439cb229e6b
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-group-name-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 404 on some group pages when name contains dot
+merge_request: 7614
+author:
diff --git a/changelogs/unreleased/faster_project_search.yml b/changelogs/unreleased/faster_project_search.yml
new file mode 100644
index 00000000000..e29a9f34ed4
--- /dev/null
+++ b/changelogs/unreleased/faster_project_search.yml
@@ -0,0 +1,4 @@
+---
+title: Faster search inside Project
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-api_owned_resource.yml b/changelogs/unreleased/feature-api_owned_resource.yml
new file mode 100644
index 00000000000..9c270e4ecf4
--- /dev/null
+++ b/changelogs/unreleased/feature-api_owned_resource.yml
@@ -0,0 +1,4 @@
+---
+title: Add api endpoint `/groups/owned`
+merge_request: 7103
+author: Borja Aparicio
diff --git a/changelogs/unreleased/feature-cycle-analytics-events.yml b/changelogs/unreleased/feature-cycle-analytics-events.yml
new file mode 100644
index 00000000000..e1211a3c774
--- /dev/null
+++ b/changelogs/unreleased/feature-cycle-analytics-events.yml
@@ -0,0 +1,4 @@
+---
+title: Add events per stage to cycle analytics
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml b/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml
new file mode 100644
index 00000000000..0441b68e45f
--- /dev/null
+++ b/changelogs/unreleased/feature-environment-teardown-when-branch-deleted.yml
@@ -0,0 +1,4 @@
+---
+title: Auto-close environment when branch is deleted
+merge_request: 7355
+author:
diff --git a/changelogs/unreleased/feature-precalculate-authorized-projects.yml b/changelogs/unreleased/feature-precalculate-authorized-projects.yml
new file mode 100644
index 00000000000..6e48b710fec
--- /dev/null
+++ b/changelogs/unreleased/feature-precalculate-authorized-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Precalculate user's authorized projects in database
+merge_request: 6839
+author:
diff --git a/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml b/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml
new file mode 100644
index 00000000000..ea336716dce
--- /dev/null
+++ b/changelogs/unreleased/feature-subscribe-to-group-level-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Allow users to subscribe to group labels
+merge_request: 7215
+author:
diff --git a/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
new file mode 100644
index 00000000000..d1bc8ea2eb1
--- /dev/null
+++ b/changelogs/unreleased/fix-404-on-network-when-entering-a-nonexistent-git-revision.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 404 on network page when entering non-existent git revision
+merge_request: 7172
+author: Hiroyuki Sato
diff --git a/changelogs/unreleased/fix-Build-timeFor.yml b/changelogs/unreleased/fix-Build-timeFor.yml
new file mode 100644
index 00000000000..ea115f7ee67
--- /dev/null
+++ b/changelogs/unreleased/fix-Build-timeFor.yml
@@ -0,0 +1,4 @@
+---
+title: Fix typo in Build page JavaScript
+merge_request: 7563
+author: winniehell
diff --git a/changelogs/unreleased/fix-admin-ci-table.yml b/changelogs/unreleased/fix-admin-ci-table.yml
new file mode 100644
index 00000000000..9a9e39ee94a
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-ci-table.yml
@@ -0,0 +1,4 @@
+---
+title: Fix misaligned buttons on admin builds page
+merge_request: 7424
+author: Didem Acet
diff --git a/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml b/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml
new file mode 100644
index 00000000000..6402d0c7ece
--- /dev/null
+++ b/changelogs/unreleased/fix-do-not-add-todo-when-build-allowed-to-fail.yml
@@ -0,0 +1,4 @@
+---
+title: Do not create a new TODO when failed build is allowed to fail
+merge_request: 7618
+author:
diff --git a/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml
new file mode 100644
index 00000000000..ad6aa214f0f
--- /dev/null
+++ b/changelogs/unreleased/fix-error-when-invalid-branch-for-new-pipeline-used.yml
@@ -0,0 +1,4 @@
+---
+title: Fix error when using invalid branch name when creating a new pipeline
+merge_request: 7324
+author:
diff --git a/changelogs/unreleased/fix-help-page-links.yml b/changelogs/unreleased/fix-help-page-links.yml
new file mode 100644
index 00000000000..9e5f41c553f
--- /dev/null
+++ b/changelogs/unreleased/fix-help-page-links.yml
@@ -0,0 +1,4 @@
+---
+title: Fix error links in help index page
+merge_request: 7396
+author: Fu Xu
diff --git a/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml b/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml
new file mode 100644
index 00000000000..a6bee989f6d
--- /dev/null
+++ b/changelogs/unreleased/fix-merge-request-screen-deleted-source-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Do not create a MergeRequestDiff record when source branch is deleted
+merge_request: 7481
+author:
diff --git a/changelogs/unreleased/fix-require-build-script-configuration-entry.yml b/changelogs/unreleased/fix-require-build-script-configuration-entry.yml
new file mode 100644
index 00000000000..00b3fd2681f
--- /dev/null
+++ b/changelogs/unreleased/fix-require-build-script-configuration-entry.yml
@@ -0,0 +1,4 @@
+---
+title: Make job script a required configuration entry
+merge_request: 7566
+author:
diff --git a/changelogs/unreleased/fix-search-input-padding.yml b/changelogs/unreleased/fix-search-input-padding.yml
new file mode 100644
index 00000000000..5d559d05d73
--- /dev/null
+++ b/changelogs/unreleased/fix-search-input-padding.yml
@@ -0,0 +1,4 @@
+---
+title: Give search-input correct padding-right value
+merge_request: 7407
+author: Philip Karpiak
diff --git a/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml b/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml
new file mode 100644
index 00000000000..56fa2170be3
--- /dev/null
+++ b/changelogs/unreleased/fix-shibboleth-auth-with-no-uid.yml
@@ -0,0 +1,4 @@
+---
+title: fix shibboleth misconfigurations resulting in authentication bypass
+merge_request: 7428
+author:
diff --git a/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml b/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml
new file mode 100644
index 00000000000..e4cf8de8699
--- /dev/null
+++ b/changelogs/unreleased/fix-singin-redirect-for-fork-new.yml
@@ -0,0 +1,5 @@
+---
+title: Fixing the issue of the project fork url giving 500 when not signed instead
+ of being redirected to sign in page
+merge_request:
+author: Cagdas Gerede
diff --git a/changelogs/unreleased/fix-slack-pipeline-event.yml b/changelogs/unreleased/fix-slack-pipeline-event.yml
new file mode 100644
index 00000000000..fec864eeb3d
--- /dev/null
+++ b/changelogs/unreleased/fix-slack-pipeline-event.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline author for Slack and use pipeline id for pipeline link
+merge_request: 7506
+author:
diff --git a/changelogs/unreleased/fix-trace-patch-updated-at.yml b/changelogs/unreleased/fix-trace-patch-updated-at.yml
new file mode 100644
index 00000000000..88f400f0a0e
--- /dev/null
+++ b/changelogs/unreleased/fix-trace-patch-updated-at.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trace patching feature - update the updated_at value
+merge_request: 7146
+author:
diff --git a/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml b/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml
new file mode 100644
index 00000000000..01b191a8c5a
--- /dev/null
+++ b/changelogs/unreleased/fix_labels_api_adding_missing_parameter.yml
@@ -0,0 +1,4 @@
+---
+title: Fix labels API by adding missing current_user parameter
+merge_request: 7458
+author: Francesco Coda Zabetta
diff --git a/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml b/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml
new file mode 100644
index 00000000000..0f7f8155f91
--- /dev/null
+++ b/changelogs/unreleased/fix_navigation_bar_issuables_counters.yml
@@ -0,0 +1,4 @@
+---
+title: Navigation bar issuables counters reflects dashboard issuables counters
+merge_request: 7368
+author: Lucas Deschamps
diff --git a/changelogs/unreleased/forking-in-progress-title.yml b/changelogs/unreleased/forking-in-progress-title.yml
new file mode 100644
index 00000000000..4b9684844b3
--- /dev/null
+++ b/changelogs/unreleased/forking-in-progress-title.yml
@@ -0,0 +1,4 @@
+---
+title: Use 'Forking in progress' title when appropriate
+merge_request: 7394
+author: Philip Karpiak
diff --git a/changelogs/unreleased/issue-13823.yml b/changelogs/unreleased/issue-13823.yml
new file mode 100644
index 00000000000..c1b5760f7df
--- /dev/null
+++ b/changelogs/unreleased/issue-13823.yml
@@ -0,0 +1,4 @@
+---
+title: Show random messages when the To Do list is empty
+merge_request: 6818
+author: Josep Llaneras
diff --git a/changelogs/unreleased/issue-24512.yml b/changelogs/unreleased/issue-24512.yml
new file mode 100644
index 00000000000..a3a9bd9c3d1
--- /dev/null
+++ b/changelogs/unreleased/issue-24512.yml
@@ -0,0 +1,4 @@
+---
+title: Add placeholder for the example text for custom hex color on label creation popup
+merge_request:
+author: Luis Alonso Chavez Armendariz
diff --git a/changelogs/unreleased/issue-boards-counter-border-fix.yml b/changelogs/unreleased/issue-boards-counter-border-fix.yml
new file mode 100644
index 00000000000..c98adb6af7c
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-counter-border-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed issue boards counter border when unauthorized
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_13232.yml b/changelogs/unreleased/issue_13232.yml
new file mode 100644
index 00000000000..6dc2de5afe4
--- /dev/null
+++ b/changelogs/unreleased/issue_13232.yml
@@ -0,0 +1,4 @@
+---
+title: Add JIRA remotelinks and prevent duplicated closing messages
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_5541.yml b/changelogs/unreleased/issue_5541.yml
new file mode 100644
index 00000000000..cf553cf8d80
--- /dev/null
+++ b/changelogs/unreleased/issue_5541.yml
@@ -0,0 +1,4 @@
+---
+title: Allow enabling and disabling commit and MR events for JIRA
+merge_request:
+author:
diff --git a/changelogs/unreleased/jira_service_simplify.yml b/changelogs/unreleased/jira_service_simplify.yml
new file mode 100644
index 00000000000..51cedd8ce5e
--- /dev/null
+++ b/changelogs/unreleased/jira_service_simplify.yml
@@ -0,0 +1,4 @@
+---
+title: simplify url generation
+merge_request:
+author: Jarka Kadlecova
diff --git a/changelogs/unreleased/ldap_check_bind.yml b/changelogs/unreleased/ldap_check_bind.yml
new file mode 100644
index 00000000000..daff8103a07
--- /dev/null
+++ b/changelogs/unreleased/ldap_check_bind.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce better credential and error checking to `rake gitlab:ldap:check`
+merge_request: 6601
+author:
diff --git a/changelogs/unreleased/less-intrusive-system-note.yml b/changelogs/unreleased/less-intrusive-system-note.yml
new file mode 100644
index 00000000000..87a5a3be246
--- /dev/null
+++ b/changelogs/unreleased/less-intrusive-system-note.yml
@@ -0,0 +1,4 @@
+---
+title: Make system notes less intrusive
+merge_request: 6755
+author:
diff --git a/changelogs/unreleased/mailroom_idle_timeout.yml b/changelogs/unreleased/mailroom_idle_timeout.yml
new file mode 100644
index 00000000000..276b28a56dd
--- /dev/null
+++ b/changelogs/unreleased/mailroom_idle_timeout.yml
@@ -0,0 +1,4 @@
+---
+title: Allow mail_room idle_timeout option to be configurable
+merge_request: 7423
+author:
diff --git a/changelogs/unreleased/master-recursiveTree.yml b/changelogs/unreleased/master-recursiveTree.yml
new file mode 100644
index 00000000000..c6384d172e2
--- /dev/null
+++ b/changelogs/unreleased/master-recursiveTree.yml
@@ -0,0 +1,4 @@
+---
+title: API: allow recursive tree request
+merge_request: 6088
+author: Rebeca Méndez
diff --git a/changelogs/unreleased/milestone-project-require.yml b/changelogs/unreleased/milestone-project-require.yml
new file mode 100644
index 00000000000..e43033541c7
--- /dev/null
+++ b/changelogs/unreleased/milestone-project-require.yml
@@ -0,0 +1,4 @@
+---
+title: Require projects before creating milestone.
+merge_request: 7301
+author: gfyoung
diff --git a/changelogs/unreleased/namespace-validation.yml b/changelogs/unreleased/namespace-validation.yml
new file mode 100644
index 00000000000..6ac461bf82e
--- /dev/null
+++ b/changelogs/unreleased/namespace-validation.yml
@@ -0,0 +1,4 @@
+---
+title: Check all namespaces on validation of new username.
+merge_request: 7537
+author:
diff --git a/changelogs/unreleased/new-note-worker-record-not-found-fix.yml b/changelogs/unreleased/new-note-worker-record-not-found-fix.yml
new file mode 100644
index 00000000000..abfba640cc0
--- /dev/null
+++ b/changelogs/unreleased/new-note-worker-record-not-found-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fix record not found error on NewNoteWorker processing
+merge_request: 6863
+author: Oswaldo Ferreira
diff --git a/changelogs/unreleased/optimize-mr-index.yml b/changelogs/unreleased/optimize-mr-index.yml
new file mode 100644
index 00000000000..1090b6d4528
--- /dev/null
+++ b/changelogs/unreleased/optimize-mr-index.yml
@@ -0,0 +1,4 @@
+---
+title: More aggressively preload on merge request and issue index pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml
new file mode 100644
index 00000000000..5e868027ed6
--- /dev/null
+++ b/changelogs/unreleased/pass-correct-tag-target-to-post-receive.yml
@@ -0,0 +1,4 @@
+---
+title: Pass correct tag target to post-receive hook when creating tag via UI
+merge_request: 7556
+author:
diff --git a/changelogs/unreleased/pipeline-notifications.yml b/changelogs/unreleased/pipeline-notifications.yml
new file mode 100644
index 00000000000..b43060674b2
--- /dev/null
+++ b/changelogs/unreleased/pipeline-notifications.yml
@@ -0,0 +1,6 @@
+---
+title: Add CI notifications. Who triggered a pipeline would receive an email after
+ the pipeline is succeeded or failed. Users could also update notification settings
+ accordingly
+merge_request: 6342
+author:
diff --git a/changelogs/unreleased/process-commits-using-sidekiq.yml b/changelogs/unreleased/process-commits-using-sidekiq.yml
new file mode 100644
index 00000000000..9f596e6a584
--- /dev/null
+++ b/changelogs/unreleased/process-commits-using-sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Process commits using a dedicated Sidekiq worker
+merge_request: 6802
+author:
diff --git a/changelogs/unreleased/rack_attack_logging.yml b/changelogs/unreleased/rack_attack_logging.yml
new file mode 100644
index 00000000000..c0d6c1fd12e
--- /dev/null
+++ b/changelogs/unreleased/rack_attack_logging.yml
@@ -0,0 +1,4 @@
+---
+title: Add logging for rack attack events to production.log
+merge_request:
+author:
diff --git a/changelogs/unreleased/related-mr-labels.yml b/changelogs/unreleased/related-mr-labels.yml
new file mode 100644
index 00000000000..268e0eab870
--- /dev/null
+++ b/changelogs/unreleased/related-mr-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Added colored labels to related MR list.
+merge_request: 7486
+author: Didem Acet
diff --git a/changelogs/unreleased/remove-heading-space-from-diff-content.yml b/changelogs/unreleased/remove-heading-space-from-diff-content.yml
new file mode 100644
index 00000000000..1ea85784d29
--- /dev/null
+++ b/changelogs/unreleased/remove-heading-space-from-diff-content.yml
@@ -0,0 +1,4 @@
+---
+title: Remove an extra leading space from diff paste data
+merge_request: 7133
+author: Hiroyuki Sato
diff --git a/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
new file mode 100644
index 00000000000..bef11c63675
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-self-from-user-model.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary self from user model
+merge_request: 7551
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/repository-name-emojis b/changelogs/unreleased/repository-name-emojis
new file mode 100644
index 00000000000..fe52df8eedc
--- /dev/null
+++ b/changelogs/unreleased/repository-name-emojis
@@ -0,0 +1,4 @@
+---
+title: Added ability to put emojis into repository name
+merge_request: 7420
+author: Vincent Composieux
diff --git a/changelogs/unreleased/setter-for-key.yml b/changelogs/unreleased/setter-for-key.yml
new file mode 100644
index 00000000000..15167904ed5
--- /dev/null
+++ b/changelogs/unreleased/setter-for-key.yml
@@ -0,0 +1,4 @@
+---
+title: Use setter for key instead AR callback
+merge_request: 7488
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/sh-bump-omniauth-gitlab.yml b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
new file mode 100644
index 00000000000..17cd5a993dd
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-omniauth-gitlab.yml
@@ -0,0 +1,4 @@
+---
+title: Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2
+merge_request:
+author:
diff --git a/changelogs/unreleased/sidekiq-job-throttling.yml b/changelogs/unreleased/sidekiq-job-throttling.yml
new file mode 100644
index 00000000000..ec4e2051c7e
--- /dev/null
+++ b/changelogs/unreleased/sidekiq-job-throttling.yml
@@ -0,0 +1,4 @@
+---
+title: Added ability to throttle Sidekiq Jobs
+merge_request: 7292
+author:
diff --git a/changelogs/unreleased/sidekiq_default_retries.yml b/changelogs/unreleased/sidekiq_default_retries.yml
new file mode 100644
index 00000000000..3df2a415dbc
--- /dev/null
+++ b/changelogs/unreleased/sidekiq_default_retries.yml
@@ -0,0 +1,4 @@
+---
+title: Set default Sidekiq retries to 3
+merge_request: 7294
+author:
diff --git a/changelogs/unreleased/sort-api-groups.yml b/changelogs/unreleased/sort-api-groups.yml
new file mode 100644
index 00000000000..e3eead8c04f
--- /dev/null
+++ b/changelogs/unreleased/sort-api-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Allow sorting groups in the API
+merge_request:
+author:
diff --git a/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml b/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml
new file mode 100644
index 00000000000..7ca0d5fb19e
--- /dev/null
+++ b/changelogs/unreleased/stanhu-gitlab-ce-fix-error-500-with-mr-images.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Error 500 when creating a merge request that contains an image that was deleted and added
+merge_request: 7457
+author:
diff --git a/changelogs/unreleased/upgrade-timeago.yml b/changelogs/unreleased/upgrade-timeago.yml
new file mode 100644
index 00000000000..ddb266ba558
--- /dev/null
+++ b/changelogs/unreleased/upgrade-timeago.yml
@@ -0,0 +1,4 @@
+---
+title: Replace jQuery.timeago with timeago.js
+merge_request: 6274
+author: ClemMakesApps
diff --git a/changelogs/unreleased/use-separate-token-for-incoming-email.yml b/changelogs/unreleased/use-separate-token-for-incoming-email.yml
new file mode 100644
index 00000000000..e498f8dd0a6
--- /dev/null
+++ b/changelogs/unreleased/use-separate-token-for-incoming-email.yml
@@ -0,0 +1,4 @@
+---
+title: Use separate email-token for incoming email and revert back the inactive feature
+merge_request: 5914
+author:
diff --git a/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml b/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml
new file mode 100644
index 00000000000..a83441b852a
--- /dev/null
+++ b/changelogs/unreleased/user-dropdown-multiple-requests-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed multiple requests sent when opening dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/user_filter_auth.yml b/changelogs/unreleased/user_filter_auth.yml
new file mode 100644
index 00000000000..e4071e22e5e
--- /dev/null
+++ b/changelogs/unreleased/user_filter_auth.yml
@@ -0,0 +1,4 @@
+---
+title: Centralize LDAP config/filter logic
+merge_request: 6606
+author:
diff --git a/changelogs/unreleased/zj-slash-commands-mattermost.yml b/changelogs/unreleased/zj-slash-commands-mattermost.yml
new file mode 100644
index 00000000000..996ffe954f3
--- /dev/null
+++ b/changelogs/unreleased/zj-slash-commands-mattermost.yml
@@ -0,0 +1,4 @@
+---
+title: Added Mattermost slash command
+merge_request: 7438
+author:
diff --git a/config/application.rb b/config/application.rb
index 946b632b0e8..fb84870dfbd 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -94,6 +94,7 @@ module Gitlab
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "environments/environments_bundle.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
diff --git a/config/database.yml.mysql b/config/database.yml.mysql
index a99c50706c5..d9702870249 100644
--- a/config/database.yml.mysql
+++ b/config/database.yml.mysql
@@ -3,8 +3,8 @@
#
production:
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_production
pool: 10
@@ -18,8 +18,8 @@ production:
#
development:
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_development
pool: 5
@@ -32,8 +32,8 @@ development:
# Do not set this db to the same as development or production.
test: &test
adapter: mysql2
- encoding: utf8
- collation: utf8_general_ci
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
reconnect: false
database: gitlabhq_test
pool: 5
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 699ab6075b6..327e4a7937c 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -138,6 +138,8 @@ production: &base
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
## Build Artifacts
artifacts:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 9fec2ad6bf7..9ddd1554811 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -215,6 +215,7 @@ Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(
Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
+Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
#
# CI
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index a0a8f88584c..a8afc36fc78 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -213,22 +213,9 @@ Devise.setup do |config|
end
if Gitlab::LDAP::Config.enabled?
- Gitlab.config.ldap.servers.values.each do |server|
- if server['allow_username_or_email_login']
- email_stripping_proc = ->(name) {name.gsub(/@.*\z/, '')}
- else
- email_stripping_proc = ->(name) {name}
- end
-
- config.omniauth server['provider_name'],
- host: server['host'],
- base: server['base'],
- uid: server['uid'],
- port: server['port'],
- method: server['method'],
- bind_dn: server['bind_dn'],
- password: server['password'],
- name_proc: email_stripping_proc
+ Gitlab::LDAP::Config.providers.each do |provider|
+ ldap_config = Gitlab::LDAP::Config.new(provider)
+ config.omniauth(provider, ldap_config.omniauth_options)
end
end
@@ -254,6 +241,10 @@ Devise.setup do |config|
end
end
+ if provider['name'] == 'shibboleth'
+ provider['args'][:fail_with_empty_uid] = true
+ end
+
# A Hash from the configuration will be passed as is.
provider_arguments << provider['args'].symbolize_keys
end
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
new file mode 100644
index 00000000000..8bb9ea29c33
--- /dev/null
+++ b/config/initializers/rack_attack_logging.rb
@@ -0,0 +1,7 @@
+# Adds logging for all Rack Attack blocks and throttling events.
+
+ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
+ if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
+ Rails.logger.info("Rack_Attack: #{req.env['rack.attack.match_type']} #{req.ip} #{req.request_method} #{req.fullpath}")
+ end
+end
diff --git a/config/initializers/routing_draw.rb b/config/initializers/routing_draw.rb
new file mode 100644
index 00000000000..25003cf0239
--- /dev/null
+++ b/config/initializers/routing_draw.rb
@@ -0,0 +1,7 @@
+# Adds draw method into Rails routing
+# It allows us to keep routing splitted into files
+class ActionDispatch::Routing::Mapper
+ def draw(routes_name)
+ instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0455a98dbfe..b87b31d9697 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -2,6 +2,9 @@
redis_config_hash = Gitlab::Redis.params
redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE
+# Default is to retry 25 times with exponential backoff. That's too much.
+Sidekiq.default_worker_options = { retry: 3 }
+
Sidekiq.configure_server do |config|
config.redis = redis_config_hash
@@ -26,6 +29,8 @@ Sidekiq.configure_server do |config|
end
Sidekiq::Cron::Job.load_from_hash! cron_jobs
+ Gitlab::SidekiqThrottler.execute!
+
# Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
diff --git a/config/mail_room.yml b/config/mail_room.yml
index b026d510f1b..774c5350a45 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -15,7 +15,7 @@
:start_tls: <%= config[:start_tls].to_json %>
:email: <%= config[:user].to_json %>
:password: <%= config[:password].to_json %>
- :idle_timeout: 60
+ :idle_timeout: <%= config[:idle_timeout].to_json %>
:name: <%= config[:mailbox].to_json %>
diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml
new file mode 100644
index 00000000000..264a975b614
--- /dev/null
+++ b/config/no_todos_messages.yml
@@ -0,0 +1,11 @@
+# When the todo list on the user's dashboard becomes empty, a random message
+# from the list below will be shown.
+#
+# If you come up with a fun one, please feel free to contribute it to GitLab!
+# https://about.gitlab.com/contributing/
+---
+- Good job! Looks like you don't have any todos left.
+- Isn't an empty todo list beautiful?
+- Give yourself a pat on the back!
+- Nothing left to do, high five!
+- Henceforth you shall be known as "Todo Destroyer".
diff --git a/config/routes.rb b/config/routes.rb
index 659ea51bc75..7bf6c03e69b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,12 +2,6 @@ require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api'
-class ActionDispatch::Routing::Mapper
- def draw(routes_name)
- instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
- end
-end
-
Rails.application.routes.draw do
concern :access_requestable do
post :request_access, on: :collection
diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb
new file mode 100644
index 00000000000..03adc4815f3
--- /dev/null
+++ b/config/routes/git_http.rb
@@ -0,0 +1,37 @@
+scope constraints: { id: /.+\.git/, format: nil } do
+ # Git HTTP clients ('git clone' etc.)
+ get '/info/refs', to: 'git_http#info_refs'
+ post '/git-upload-pack', to: 'git_http#git_upload_pack'
+ post '/git-receive-pack', to: 'git_http#git_receive_pack'
+
+ # Git LFS API (metadata)
+ post '/info/lfs/objects/batch', to: 'lfs_api#batch'
+ post '/info/lfs/objects', to: 'lfs_api#deprecated'
+ get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+
+ # GitLab LFS object storage
+ scope constraints: { oid: /[a-f0-9]{64}/ } do
+ get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+
+ scope constraints: { size: /[0-9]+/ } do
+ put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
+ put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+ end
+ end
+end
+
+# Allow /info/refs, /info/refs?service=git-upload-pack, and
+# /info/refs?service=git-receive-pack, but nothing else.
+#
+git_http_handshake = lambda do |request|
+ request.query_string.blank? ||
+ request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
+end
+
+ref_redirect = redirect do |params, request|
+ path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+ path << "?#{request.query_string}" unless request.query_string.blank?
+ path
+end
+
+get '/info/refs', constraints: git_http_handshake, to: ref_redirect
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 826048ba196..a3a001178b4 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -3,7 +3,7 @@ require 'constraints/group_url_constrainer'
constraints(GroupUrlConstrainer.new) do
scope(path: ':id',
as: :group,
- constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ constraints: { id: Gitlab::Regex.namespace_route_regex },
controller: :groups) do
get '/', action: :show
patch '/', action: :update
@@ -12,26 +12,34 @@ constraints(GroupUrlConstrainer.new) do
end
end
-scope constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
- resources :groups, except: [:show] do
- member do
- get :issues
- get :merge_requests
- get :projects
- get :activity
- end
+resources :groups, only: [:index, :new, :create]
- scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
- end
+scope(path: 'groups/:id',
+ controller: :groups,
+ constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+ get :edit, as: :edit_group
+ get :issues, as: :issues_group
+ get :merge_requests, as: :merge_requests_group
+ get :projects, as: :projects_group
+ get :activity, as: :activity_group
+end
+
+scope(path: 'groups/:group_id',
+ module: :groups,
+ as: :group,
+ constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
- resource :avatar, only: [:destroy]
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+ resource :avatar, only: [:destroy]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- resources :labels, except: [:show], constraints: { id: /\d+/ }
- end
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
+ post :toggle_subscription, on: :member
end
- get 'groups/:id' => 'groups#show', as: :group_canonical
end
+
+# Must be last route in this file
+get 'groups/:id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index 4cb68c9b34a..6b91485da9e 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -4,6 +4,7 @@ resource :profile, only: [:show, :update] do
get :applications, to: 'oauth/applications#index'
put :reset_private_token
+ put :reset_incoming_email_token
put :update_username
end
@@ -22,6 +23,12 @@ resource :profile, only: [:show, :update] do
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
+ resources :chat_names, only: [:index, :new, :create, :destroy] do
+ collection do
+ delete :deny
+ end
+ end
+
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 8142e231621..d6eae1c9fce 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -18,152 +18,17 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get :autocomplete_sources
get :activity
get :refs
+ put :new_issue_address
end
scope module: :projects do
- scope constraints: { id: /.+\.git/, format: nil } do
- # Git HTTP clients ('git clone' etc.)
- get '/info/refs', to: 'git_http#info_refs'
- post '/git-upload-pack', to: 'git_http#git_upload_pack'
- post '/git-receive-pack', to: 'git_http#git_receive_pack'
-
- # Git LFS API (metadata)
- post '/info/lfs/objects/batch', to: 'lfs_api#batch'
- post '/info/lfs/objects', to: 'lfs_api#deprecated'
- get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
-
- # GitLab LFS object storage
- scope constraints: { oid: /[a-f0-9]{64}/ } do
- get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
-
- scope constraints: { size: /[0-9]+/ } do
- put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
- put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
- end
- end
- end
-
- # Allow /info/refs, /info/refs?service=git-upload-pack, and
- # /info/refs?service=git-receive-pack, but nothing else.
- #
- git_http_handshake = lambda do |request|
- request.query_string.blank? ||
- request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
- end
-
- ref_redirect = redirect do |params, request|
- path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
- path << "?#{request.query_string}" unless request.query_string.blank?
- path
- end
-
- get '/info/refs', constraints: git_http_handshake, to: ref_redirect
-
- # Blob routes:
- get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
- post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
- get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
- put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
- post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+ draw :git_http
#
# Templates
#
get '/templates/:template_type/:key' => 'templates#show', as: :template
- scope do
- get(
- '/blob/*id/diff',
- to: 'blob#diff',
- constraints: { id: /.+/, format: false },
- as: :blob_diff
- )
- get(
- '/blob/*id',
- to: 'blob#show',
- constraints: { id: /.+/, format: false },
- as: :blob
- )
- delete(
- '/blob/*id',
- to: 'blob#destroy',
- constraints: { id: /.+/, format: false }
- )
- put(
- '/blob/*id',
- to: 'blob#update',
- constraints: { id: /.+/, format: false }
- )
- post(
- '/blob/*id',
- to: 'blob#create',
- constraints: { id: /.+/, format: false }
- )
- end
-
- scope do
- get(
- '/raw/*id',
- to: 'raw#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :raw
- )
- end
-
- scope do
- get(
- '/tree/*id',
- to: 'tree#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :tree
- )
- end
-
- scope do
- get(
- '/find_file/*id',
- to: 'find_file#show',
- constraints: { id: /.+/, format: /html/ },
- as: :find_file
- )
- end
-
- scope do
- get(
- '/files/*id',
- to: 'find_file#list',
- constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
- as: :files
- )
- end
-
- scope do
- post(
- '/create_dir/*id',
- to: 'tree#create_dir',
- constraints: { id: /.+/ },
- as: 'create_dir'
- )
- end
-
- scope do
- get(
- '/blame/*id',
- to: 'blame#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :blame
- )
- end
-
- scope do
- get(
- '/commits/*id',
- to: 'commits#show',
- constraints: { id: /.+/, format: false },
- as: :commits
- )
- end
-
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
@@ -206,29 +71,6 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
- WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
-
- scope do
- # Order matters to give priority to these matches
- get '/wikis/git_access', to: 'wikis#git_access'
- get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
- post '/wikis', to: 'wikis#create'
-
- get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
- get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
-
- get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
- delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
- put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
- post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
- end
-
- resource :repository, only: [:create] do
- member do
- get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
- end
- end
-
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
get :test
@@ -245,23 +87,6 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
- resources :refs, only: [] do
- collection do
- get 'switch'
- end
-
- member do
- # tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
- # Directories with leading dots erroneously get rejected if git
- # ref regex used in constraints. Regex verification now done in controller.
- get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
- id: /.*/,
- path: /.*/
- }
- end
- end
-
resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
@@ -300,6 +125,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
@@ -327,6 +153,18 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
resource :cycle_analytics, only: [:show]
+ namespace :cycle_analytics do
+ scope :events, controller: 'events' do
+ get :issue
+ get :plan
+ get :code
+ get :test
+ get :review
+ get :staging
+ get :production
+ end
+ end
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -467,6 +305,11 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
end
end
end
+
+ # Since both wiki and repository routing contains wildcard characters
+ # its preferable to keep it below all other project routes
+ draw :wiki
+ draw :repository
end
end
end
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
new file mode 100644
index 00000000000..76dcf113aea
--- /dev/null
+++ b/config/routes/repository.rb
@@ -0,0 +1,110 @@
+# All routing related to repositoty browsing
+
+resource :repository, only: [:create] do
+ member do
+ get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
+ end
+end
+
+resources :refs, only: [] do
+ collection do
+ get 'switch'
+ end
+
+ member do
+ # tree viewer logs
+ get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
+ get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
+ id: /.*/,
+ path: /.*/
+ }
+ end
+end
+
+get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
+post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
+get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
+put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
+post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+
+scope do
+ get(
+ '/blob/*id/diff',
+ to: 'blob#diff',
+ constraints: { id: /.+/, format: false },
+ as: :blob_diff
+ )
+ get(
+ '/blob/*id',
+ to: 'blob#show',
+ constraints: { id: /.+/, format: false },
+ as: :blob
+ )
+ delete(
+ '/blob/*id',
+ to: 'blob#destroy',
+ constraints: { id: /.+/, format: false }
+ )
+ put(
+ '/blob/*id',
+ to: 'blob#update',
+ constraints: { id: /.+/, format: false }
+ )
+ post(
+ '/blob/*id',
+ to: 'blob#create',
+ constraints: { id: /.+/, format: false }
+ )
+
+ get(
+ '/raw/*id',
+ to: 'raw#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :raw
+ )
+
+ get(
+ '/tree/*id',
+ to: 'tree#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :tree
+ )
+
+ get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+ )
+
+ get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+ )
+
+ post(
+ '/create_dir/*id',
+ to: 'tree#create_dir',
+ constraints: { id: /.+/ },
+ as: 'create_dir'
+ )
+
+ get(
+ '/blame/*id',
+ to: 'blame#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :blame
+ )
+
+ # File/dir history
+ get(
+ '/commits/*id',
+ to: 'commits#show',
+ constraints: { id: /.+/, format: false },
+ as: :commits
+ )
+end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 0a9c924863d..dc1068af6f6 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -14,31 +14,32 @@ end
constraints(UserUrlConstrainer.new) do
scope(path: ':username',
as: :user,
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ constraints: { username: Gitlab::Regex.namespace_route_regex },
controller: :users) do
get '/', action: :show
end
end
-scope(path: 'users/:username',
- as: :user,
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
- controller: :users) do
- get :calendar
- get :calendar_activities
- get :groups
- get :projects
- get :contributed, as: :contributed_projects
- get :snippets
- get :exists
- get '/', to: redirect('/%{username}')
-end
+scope(constraints: { username: Gitlab::Regex.namespace_route_regex }) do
+ scope(path: 'users/:username',
+ as: :user,
+ controller: :users) do
+ get :calendar
+ get :calendar_activities
+ get :groups
+ get :projects
+ get :contributed, as: :contributed_projects
+ get :snippets
+ get :exists
+ get '/', to: redirect('/%{username}')
+ end
-# Compatibility with old routing
-# TODO (dzaporozhets): remove in 10.0
-get '/u/:username', to: redirect('/%{username}'), constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
-# TODO (dzaporozhets): remove in 9.0
-get '/u/:username/groups', to: redirect('/users/%{username}/groups'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
-get '/u/:username/projects', to: redirect('/users/%{username}/projects'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
-get '/u/:username/snippets', to: redirect('/users/%{username}/snippets'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
-get '/u/:username/contributed', to: redirect('/users/%{username}/contributed'), constraints: { username: /[a-zA-Z.0-9_\-]+/ }
+ # Compatibility with old routing
+ # TODO (dzaporozhets): remove in 10.0
+ get '/u/:username', to: redirect('/%{username}')
+ # TODO (dzaporozhets): remove in 9.0
+ get '/u/:username/groups', to: redirect('/users/%{username}/groups')
+ get '/u/:username/projects', to: redirect('/users/%{username}/projects')
+ get '/u/:username/snippets', to: redirect('/users/%{username}/snippets')
+ get '/u/:username/contributed', to: redirect('/users/%{username}/contributed')
+end
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
new file mode 100644
index 00000000000..ecd4d395d66
--- /dev/null
+++ b/config/routes/wiki.rb
@@ -0,0 +1,16 @@
+WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+
+scope do
+ # Order matters to give priority to these matches
+ get '/wikis/git_access', to: 'wikis#git_access'
+ get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis', to: 'wikis#create'
+
+ get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
+ get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
+
+ get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
+ delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
+ put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
+ post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
+end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index f36fe893fd0..69136b73946 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -21,6 +21,7 @@
- [post_receive, 5]
- [merge, 5]
- [update_merge_requests, 3]
+ - [process_commit, 2]
- [new_note, 2]
- [build, 2]
- [pipeline, 2]
@@ -33,6 +34,8 @@
- [project_service, 1]
- [clear_database_cache, 1]
- [delete_user, 1]
+ - [delete_merged_branches, 1]
+ - [authorized_projects, 1]
- [expire_build_instance_artifacts, 1]
- [group_destroy, 1]
- [irker, 1]
diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example
index e5058cebce8..40a16a32359 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -44,7 +44,7 @@ listen "127.0.0.1:8080", :tcp_nopush => true
# nuke workers after 30 seconds instead of 60 seconds (the default)
#
# NOTICE: git push over http depends on this value.
-# If you want be able to push huge amount of data to git repository over http
+# If you want to be able to push huge amount of data to git repository over http
# you will have to increase this value too.
#
# Example of output if you try to push 1GB repo to GitLab over http.
@@ -82,7 +82,7 @@ GC.respond_to?(:copy_on_write_friendly=) and
check_client_connection false
before_fork do |server, worker|
- # the following is highly recomended for Rails + "preload_app true"
+ # the following is highly recommended for Rails + "preload_app true"
# as there's no need for the master process to hold a connection
defined?(ActiveRecord::Base) and
ActiveRecord::Base.connection.disconnect!
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index a984eda5ab5..18a2df7c059 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -1,4 +1,5 @@
require 'sidekiq/testing'
+require './db/fixtures/support/serialized_transaction'
Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index e882a492757..7b3908fae98 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -1,5 +1,6 @@
require 'sidekiq/testing'
require './spec/support/test_env'
+require './db/fixtures/support/serialized_transaction'
class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false)
@@ -203,6 +204,8 @@ class Gitlab::Seeder::CycleAnalytics
pipeline.run!
Timecop.travel rand(1..6).hours.from_now
pipeline.succeed!
+
+ PipelineMetricsWorker.new.perform(pipeline.id)
end
end
diff --git a/db/fixtures/support/serialized_transaction.rb b/db/fixtures/support/serialized_transaction.rb
new file mode 100644
index 00000000000..d3305b661e5
--- /dev/null
+++ b/db/fixtures/support/serialized_transaction.rb
@@ -0,0 +1,9 @@
+require 'gitlab/database'
+
+module Gitlab
+ module Database
+ def self.serialized_transaction
+ connection.transaction { yield }
+ end
+ end
+end
diff --git a/db/fixtures/test/001_repo.rb b/db/fixtures/test/001_repo.rb
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/db/fixtures/test/001_repo.rb
+++ /dev/null
diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
new file mode 100644
index 00000000000..f2cf956adc9
--- /dev/null
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_column :users, :incoming_email_token, :string
+ add_concurrent_index :users, :incoming_email_token
+ end
+end
diff --git a/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
index fad62d716b3..4da5ec9bd28 100644
--- a/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
+++ b/db/migrate/20160914131004_only_allow_merge_if_all_discussions_are_resolved.rb
@@ -5,10 +5,7 @@ class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:projects,
- :only_allow_merge_if_all_discussions_are_resolved,
- :boolean,
- default: false)
+ add_column :projects, :only_allow_merge_if_all_discussions_are_resolved, :boolean
end
def down
diff --git a/db/migrate/20161010142410_create_project_authorizations.rb b/db/migrate/20161010142410_create_project_authorizations.rb
new file mode 100644
index 00000000000..e095ab969f8
--- /dev/null
+++ b/db/migrate/20161010142410_create_project_authorizations.rb
@@ -0,0 +1,15 @@
+class CreateProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_authorizations do |t|
+ t.references :user, foreign_key: { on_delete: :cascade }
+ t.references :project, foreign_key: { on_delete: :cascade }
+ t.integer :access_level
+
+ t.index [:user_id, :project_id, :access_level], unique: true, name: 'index_project_authorizations_on_user_id_project_id_access_level'
+ end
+ end
+end
diff --git a/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb b/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb
new file mode 100644
index 00000000000..8f6be9dd677
--- /dev/null
+++ b/db/migrate/20161017091941_add_authorized_projects_populated_to_users.rb
@@ -0,0 +1,9 @@
+class AddAuthorizedProjectsPopulatedToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :authorized_projects_populated, :boolean
+ end
+end
diff --git a/db/migrate/20161020075734_default_request_access_groups.rb b/db/migrate/20161020075734_default_request_access_groups.rb
new file mode 100644
index 00000000000..9721cc88724
--- /dev/null
+++ b/db/migrate/20161020075734_default_request_access_groups.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :namespaces, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :namespaces, :request_access_enabled, true
+ end
+end
diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb
new file mode 100644
index 00000000000..cb790291b24
--- /dev/null
+++ b/db/migrate/20161020075830_default_request_access_projects.rb
@@ -0,0 +1,12 @@
+class DefaultRequestAccessProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ def up
+ change_column_default :projects, :request_access_enabled, false
+ end
+
+ def down
+ change_column_default :projects, :request_access_enabled, true
+ end
+end
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
new file mode 100644
index 00000000000..f49df6802a7
--- /dev/null
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding a foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :merge_request_metrics, :pipeline_id, :integer
+ add_concurrent_index :merge_request_metrics, :pipeline_id
+ add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
new file mode 100644
index 00000000000..97534679b59
--- /dev/null
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -0,0 +1,14 @@
+class AddProjectIdToSubscriptions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :subscriptions, :project_id, :integer
+ add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ remove_column :subscriptions, :project_id
+ end
+end
diff --git a/db/migrate/20161031174110_migrate_subscriptions_project_id.rb b/db/migrate/20161031174110_migrate_subscriptions_project_id.rb
new file mode 100644
index 00000000000..549145a0a65
--- /dev/null
+++ b/db/migrate/20161031174110_migrate_subscriptions_project_id.rb
@@ -0,0 +1,44 @@
+class MigrateSubscriptionsProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Subscriptions will not work as expected until this migration is complete.'
+
+ def up
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT issues.project_id
+ FROM issues
+ WHERE issues.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'Issue';
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT merge_requests.target_project_id
+ FROM merge_requests
+ WHERE merge_requests.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'MergeRequest';
+ EOF
+
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions
+ SET project_id = (
+ SELECT projects.id
+ FROM labels INNER JOIN projects ON projects.id = labels.project_id
+ WHERE labels.id = subscriptions.subscribable_id
+ )
+ WHERE subscriptions.subscribable_type = 'Label';
+ EOF
+ end
+
+ def down
+ execute <<-EOF.strip_heredoc
+ UPDATE subscriptions SET project_id = NULL;
+ EOF
+ end
+end
diff --git a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
new file mode 100644
index 00000000000..4b1b29e1265
--- /dev/null
+++ b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
@@ -0,0 +1,18 @@
+class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because it changes a column to not accept null values.'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id, :project_id], { unique: true, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' }
+ remove_index :subscriptions, name: 'subscriptions_user_id_and_ref_fields' if index_name_exists?(:subscriptions, 'subscriptions_user_id_and_ref_fields', false)
+ end
+
+ def down
+ add_concurrent_index :subscriptions, [:subscribable_id, :subscribable_type, :user_id], { unique: true, name: 'subscriptions_user_id_and_ref_fields' }
+ remove_index :subscriptions, name: 'index_subscriptions_on_subscribable_and_user_id_and_project_id' if index_name_exists?(:subscriptions, 'index_subscriptions_on_subscribable_and_user_id_and_project_id', false)
+ end
+end
diff --git a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
new file mode 100644
index 00000000000..e644a174964
--- /dev/null
+++ b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
@@ -0,0 +1,31 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :application_settings, :sidekiq_throttling_enabled, :boolean, default: false
+ add_column :application_settings, :sidekiq_throttling_queues, :string
+ add_column :application_settings, :sidekiq_throttling_factor, :decimal
+ end
+end
diff --git a/db/migrate/20161113184239_create_user_chat_names_table.rb b/db/migrate/20161113184239_create_user_chat_names_table.rb
new file mode 100644
index 00000000000..97b597654f7
--- /dev/null
+++ b/db/migrate/20161113184239_create_user_chat_names_table.rb
@@ -0,0 +1,21 @@
+class CreateUserChatNamesTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :chat_names do |t|
+ t.integer :user_id, null: false
+ t.integer :service_id, null: false
+ t.string :team_id, null: false
+ t.string :team_domain
+ t.string :chat_id, null: false
+ t.string :chat_name
+ t.datetime :last_used_at
+ t.timestamps null: false
+ end
+
+ add_index :chat_names, [:user_id, :service_id], unique: true
+ add_index :chat_names, [:service_id, :team_id, :chat_id], unique: true
+ end
+end
diff --git a/db/migrate/20161117114805_remove_undeleted_groups.rb b/db/migrate/20161117114805_remove_undeleted_groups.rb
new file mode 100644
index 00000000000..ebc2d974ae0
--- /dev/null
+++ b/db/migrate/20161117114805_remove_undeleted_groups.rb
@@ -0,0 +1,16 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUndeletedGroups < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute "DELETE FROM namespaces WHERE deleted_at IS NOT NULL;"
+ end
+
+ def down
+ # This is an irreversible migration;
+ # If someone is trying to rollback for other reasons, we should not throw an Exception.
+ # raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/migrate/20161118183841_add_commit_events_to_services.rb b/db/migrate/20161118183841_add_commit_events_to_services.rb
new file mode 100644
index 00000000000..4f9b5dd2281
--- /dev/null
+++ b/db/migrate/20161118183841_add_commit_events_to_services.rb
@@ -0,0 +1,15 @@
+class AddCommitEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :commit_events, :boolean, default: true, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :commit_events)
+ end
+end
diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb
index 319d86ac159..319d86ac159 100644
--- a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
+++ b/db/post_migrate/20161011222551_remove_inactive_jira_service_properties.rb
diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
new file mode 100644
index 00000000000..df38591a333
--- /dev/null
+++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb
@@ -0,0 +1,49 @@
+class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ BATCH_SIZE = 500
+ DOWNTIME = false
+
+ # This migration is idempotent and there's no sense in throwing away the
+ # partial result if it's interrupted
+ disable_ddl_transaction!
+
+ def up
+ projects = Arel::Table.new(:projects)
+ namespaces = Arel::Table.new(:namespaces)
+
+ finder_sql =
+ projects.
+ join(namespaces, Arel::Nodes::InnerJoin).
+ on(projects[:namespace_id].eq(namespaces[:id])).
+ where(projects[:visibility_level].gt(namespaces[:visibility_level])).
+ project(projects[:id], namespaces[:visibility_level]).
+ take(BATCH_SIZE).
+ to_sql
+
+ # Update matching rows in batches. Each batch can cause up to 3 UPDATE
+ # statements, in addition to the SELECT: one per visibility_level
+ loop do
+ to_update = connection.exec_query(finder_sql)
+ break if to_update.rows.count == 0
+
+ # row[0] is projects.id, row[1] is namespaces.visibility_level
+ updates = to_update.rows.each_with_object(Hash.new {|h, k| h[k] = [] }) do |row, obj|
+ obj[row[1]] << row[0]
+ end
+
+ updates.each do |visibility_level, project_ids|
+ updater = Arel::UpdateManager.new(ActiveRecord::Base).
+ table(projects).
+ set(projects[:visibility_level] => visibility_level).
+ where(projects[:id].in(project_ids))
+
+ ActiveRecord::Base.connection.exec_update(updater.to_sql, self.class.name, [])
+ end
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 60e7b26e29e..6b28e80f01d 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: 20161106185620) do
+ActiveRecord::Schema.define(version: 20161118183841) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,6 +98,9 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
@@ -149,6 +152,21 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "message_html"
end
+ create_table "chat_names", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "service_id", null: false
+ t.string "team_id", null: false
+ t.string "team_domain"
+ t.string "chat_id", null: false
+ t.string "chat_name"
+ t.datetime "last_used_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "chat_names", ["service_id", "team_id", "chat_id"], name: "index_chat_names_on_service_id_and_team_id_and_chat_id", unique: true, using: :btree
+ add_index "chat_names", ["user_id", "service_id"], name: "index_chat_names_on_user_id_and_service_id", unique: true, using: :btree
+
create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
@@ -631,10 +649,12 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.datetime "merged_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "pipeline_id"
end
add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+ add_index "merge_request_metrics", ["pipeline_id"], name: "index_merge_request_metrics_on_pipeline_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
@@ -720,7 +740,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.string "avatar"
t.boolean "share_with_group_lock", default: false
t.integer "visibility_level", default: 20, null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
t.text "description_html"
@@ -841,6 +861,14 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
+ create_table "project_authorizations", force: :cascade do |t|
+ t.integer "user_id"
+ t.integer "project_id"
+ t.integer "access_level"
+ end
+
+ add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -908,11 +936,11 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
- t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "request_access_enabled", default: false, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
t.text "description_html"
- t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false
+ t.boolean "only_allow_merge_if_all_discussions_are_resolved"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1003,6 +1031,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: true, null: false
+ t.boolean "commit_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -1049,9 +1078,10 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "project_id"
end
- add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
+ add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], name: "index_subscriptions_on_subscribable_and_user_id_and_project_id", unique: true, using: :btree
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
@@ -1183,6 +1213,8 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
t.string "organization"
+ t.string "incoming_email_token"
+ t.boolean "authorized_projects_populated"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1192,6 +1224,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+ add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
@@ -1239,12 +1272,16 @@ ActiveRecord::Schema.define(version: 20161106185620) do
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "project_authorizations", "projects", on_delete: :cascade
+ add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
+ add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index c30bf328003..66c8c26e4f0 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -21,6 +21,7 @@
- [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.
## Administrator documentation
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index bf7814875bf..d3f216fb3bf 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -35,6 +35,10 @@ of one hour.
To enable LDAP integration you need to add your LDAP server settings in
`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
+There is a Rake task to check LDAP configuration. After configuring LDAP
+using the documentation below, see [LDAP check Rake task](../raketasks/check.md#ldap-check)
+for information on the LDAP check Rake task.
+
>**Note**: In GitLab EE, you can configure multiple LDAP servers to connect to
one GitLab server.
@@ -253,6 +257,24 @@ the LDAP server's SSL certificate is performed.
## Troubleshooting
+### Debug LDAP user filter with ldapsearch
+
+This example uses ldapsearch and assumes you are using ActiveDirectory. The
+following query returns the login names of the users that will be allowed to
+log in to GitLab if you configure your own user_filter.
+
+```
+ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$base" "$user_filter" sAMAccountName
+```
+
+- Variables beginning with a `$` refer to a variable from the LDAP section of
+ your configuration file.
+- Replace ldaps:// with ldap:// if you are using the plain authentication method.
+ Port `389` is the default `ldap://` port and `636` is the default `ldaps://`
+ port.
+- We are assuming the password for the bind_dn user is in bind_dn_password.txt.
+
+
### Invalid credentials when logging in
- Make sure the user you are binding with has enough permissions to read the user's
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index d74a786ac24..d5a5aef7ec0 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -7,19 +7,10 @@ highly available.
## Architecture
-### Active/Passive
-
-For pure high-availability/failover with no scaling you can use an
-active/passive configuration. This utilizes DRBD (Distributed Replicated
-Block Device) to keep all data in sync. DRBD requires a low latency link to
-remain in sync. It is not advisable to attempt to run DRBD between data centers
-or in different cloud availability zones.
+There are two kinds of setups:
-Components/Servers Required:
-
-- 2 servers/virtual machines (one active/one passive)
-
-![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
+- active/active
+- active/passive
### Active/Active
@@ -28,12 +19,24 @@ user requests simultaneously. The database, Redis, and GitLab application are
all deployed on separate servers. The configuration is **only** highly-available
if the database, Redis and storage are also configured as such.
-![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
-
-**Steps to configure active/active:**
+Follow the steps below to configure an active/active setup:
1. [Configure the database](database.md)
1. [Configure Redis](redis.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
+
+![Active/Active HA Diagram](../img/high_availability/active-active-diagram.png)
+
+### Active/Passive
+
+For pure high-availability/failover with no scaling you can use an
+active/passive configuration. This utilizes DRBD (Distributed Replicated
+Block Device) to keep all data in sync. DRBD requires a low latency link to
+remain in sync. It is not advisable to attempt to run DRBD between data centers
+or in different cloud availability zones.
+
+Components/Servers Required: 2 servers/virtual machines (one active/one passive)
+
+![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png)
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 537f4f3501d..5602d70f1ef 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -76,7 +76,7 @@ configuration to move each data location to a subdirectory:
user['home'] = '/gitlab-data/home'
git_data_dir '/gitlab-data/git-data'
gitlab_rails['shared_path'] = '/gitlab-data/shared'
-gitlab_rails['uploads_directory'] = "/gitlab-data/uploads"
+gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
gitlab_ci['builds_directory'] = '/gitlab-data/builds'
```
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index bc424330656..f532a106bc6 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,265 +1,780 @@
# Configuring Redis for GitLab HA
-You can choose to install and manage Redis yourself, or you can use the one
-that comes bundled with GitLab Omnibus packages.
-
-> **Note:** Redis does not require authentication by default. See
+>
+Experimental Redis Sentinel support was [Introduced][ce-1877] in GitLab 8.11.
+Starting with 8.14, Redis Sentinel is no longer experimental.
+If you've used it with versions `< 8.14` before, please check the updated
+documentation here.
+
+High Availability with [Redis] is possible using a **Master** x **Slave**
+topology with a [Redis Sentinel][sentinel] service to watch and automatically
+start the failover procedure.
+
+You can choose to install and manage Redis and Sentinel yourself, use
+a hosted cloud solution or you can use the one that comes bundled with
+Omnibus GitLab packages.
+
+> **Notes:**
+- Redis requires authentication for High Availability. See
[Redis Security](http://redis.io/topics/security) documentation for more
information. We recommend using a combination of a Redis password and tight
firewall rules to secure your Redis service.
+- You are highly encouraged to read the [Redis Sentinel][sentinel] documentation
+ before configuring Redis HA with GitLab to fully understand the topology and
+ architecture.
+- This is the documentation for the Omnibus GitLab packages. For installations
+ from source, follow the [Redis HA source installation](redis_source.md) guide.
+- Redis Sentinel daemon is bundled with Omnibus GitLab Enterprise Edition only.
+ For configuring Sentinel with the Omnibus GitLab Community Edition and
+ installations from source, read the
+ [Available configuration setups](#available-configuration-setups) section
+ below.
+
+## Overview
+
+Before diving into the details of setting up Redis and Redis Sentinel for HA,
+make sure you read this Overview section to better understand how the components
+are tied together.
+
+You need at least `3` independent machines: physical, or VMs running into
+distinct physical machines. It is essential that all master and slaves Redis
+instances run in different machines. If you fail to provision the machines in
+that specific way, any issue with the shared environment can bring your entire
+setup down.
+
+It is OK to run a Sentinel along with a master or slave Redis instance.
+No more than one Sentinel in the same machine though.
+
+You also need to take in consideration the underlying network topology,
+making sure you have redundant connectivity between Redis / Sentinel and
+GitLab instances, otherwise the networks will become a single point of
+failure.
+
+Make sure that you read this document once as a whole before configuring the
+components below.
+
+### High Availability with Sentinel
+
+>**Notes:**
+- Starting with GitLab `8.11`, you can configure a list of Redis Sentinel
+ servers that will monitor a group of Redis servers to provide failover support.
+- Starting with GitLab `8.14`, the Omnibus GitLab Enterprise Edition package
+ comes with Redis Sentinel daemon built-in.
+
+High Availability with Redis requires a few things:
+
+- Multiple Redis instances
+- Run Redis in a **Master** x **Slave** topology
+- Multiple Sentinel instances
+- Application support and visibility to all Sentinel and Redis instances
+
+Redis Sentinel can handle the most important tasks in an HA environment and that's
+to help keep servers online with minimal to no downtime. Redis Sentinel:
+
+- Monitors **Master** and **Slaves** instances to see if they are available
+- Promotes a **Slave** to **Master** when the **Master** fails
+- Demotes a **Master** to **Slave** when the failed **Master** comes back online
+ (to prevent data-partitioning)
+- Can be queried by the application to always connect to the current **Master**
+ server
+
+When a **Master** fails to respond, it's the application's responsibility
+(in our case GitLab) to handle timeout and reconnect (querying a **Sentinel**
+for a new **Master**).
-## Configure your own Redis server
+To get a better understanding on how to correctly setup Sentinel, please read
+the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
+failing to configure it correctly can lead to data loss or can bring your
+whole cluster down, invalidating the failover effort.
-If you're hosting GitLab on a cloud provider, you can optionally use a
-managed service for Redis. For example, AWS offers a managed ElastiCache service
-that runs Redis.
+### Recommended setup
-## Configure Redis using Omnibus
+For a minimal setup, you will install the Omnibus GitLab package in `3`
+**independent** machines, both with **Redis** and **Sentinel**:
-If you don't want to bother setting up your own Redis server, you can use the
-one bundled with Omnibus. In this case, you should disable all services except
-Redis.
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Download/install GitLab Omnibus using **steps 1 and 2** from
- [GitLab downloads](https://about.gitlab.com/downloads). Do not complete other
- steps on the download page.
-1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
- Be sure to change the `external_url` to match your eventual GitLab front-end
- URL:
+If you are not sure or don't understand why and where the amount of nodes come
+from, read [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview).
- ```ruby
- external_url 'https://gitlab.example.com'
-
- # Disable all services except Redis
- redis['enable'] = true
- bootstrap['enable'] = false
- nginx['enable'] = false
- unicorn['enable'] = false
- sidekiq['enable'] = false
- postgresql['enable'] = false
- gitlab_workhorse['enable'] = false
- mailroom['enable'] = false
-
- # Redis configuration
- redis['port'] = 6379
- redis['bind'] = '0.0.0.0'
+For a recommended setup that can resist more failures, you will install
+the Omnibus GitLab package in `5` **independent** machines, both with
+**Redis** and **Sentinel**:
- # If you wish to use Redis authentication (recommended)
- redis['password'] = 'Redis Password'
- ```
+- Redis Master + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
+- Redis Slave + Sentinel
-1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
+### Redis setup overview
- > **Note**: This `reconfigure` step will result in some errors.
- That's OK - don't be alarmed.
+You must have at least `3` Redis servers: `1` Master, `2` Slaves, and they
+need to be each in a independent machine (see explanation above).
-1. Run `touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
- from running on upgrade. Only the primary GitLab application server should
- handle migrations.
+You can have additional Redis nodes, that will help survive a situation
+where more nodes goes down. Whenever there is only `2` nodes online, a failover
+will not be initiated.
-## Experimental Redis Sentinel support
+As an example, if you have `6` Redis nodes, a maximum of `3` can be
+simultaneously down.
-> [Introduced][ce-1877] in GitLab 8.11.
+Please note that there are different requirements for Sentinel nodes.
+If you host them in the same Redis machines, you may need to take
+that restrictions into consideration when calculating the amount of
+nodes to be provisioned. See [Sentinel setup overview](#sentinel-setup-overview)
+documentation for more information.
-Since GitLab 8.11, you can configure a list of Redis Sentinel servers that
-will monitor a group of Redis servers to provide you with a standard failover
-support.
+All Redis nodes should be configured the same way and with similar server specs, as
+in a failover situation, any **Slave** can be promoted as the new **Master** by
+the Sentinel servers.
-There is currently one exception to the Sentinel support: `mail_room`, the
-component that processes incoming emails. It doesn't support Sentinel yet, but
-we hope to integrate a future release that does support it.
+The replication requires authentication, so you need to define a password to
+protect all Redis nodes and the Sentinels. They will all share the same
+password, and all instances must be able to talk to
+each other over the network.
-To get a better understanding on how to correctly setup Sentinel, please read
-the [Redis Sentinel documentation](http://redis.io/topics/sentinel) first, as
-failing to configure it correctly can lead to data loss.
+### Sentinel setup overview
-The configuration consists of three parts:
+Sentinels watch both other Sentinels and Redis nodes. Whenever a Sentinel
+detects that a Redis node is not responding, it will announce that to the
+other Sentinels. They have to reach the **quorum**, that is the minimum amount
+of Sentinels that agrees a node is down, in order to be able to start a failover.
-- Redis setup
-- Sentinel setup
-- GitLab setup
+Whenever the **quorum** is met, the **majority** of all known Sentinel nodes
+need to be available and reachable, so that they can elect the Sentinel **leader**
+who will take all the decisions to restore the service availability by:
-Read carefully how to configure those components below.
+- Promoting a new **Master**
+- Reconfiguring the other **Slaves** and make them point to the new **Master**
+- Announce the new **Master** to every other Sentinel peer
+- Reconfigure the old **Master** and demote to **Slave** when it comes back online
-### Redis setup
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in a independent machine (that are believed to fail independently),
+ideally in different geographical areas.
-You must have at least 2 Redis servers: 1 Master, 1 or more Slaves.
-They should be configured the same way and with similar server specs, as
-in a failover situation, any Slave can be elected as the new Master by
-the Sentinel servers.
+You can configure them in the same machines where you've configured the other
+Redis servers, but understand that if a whole node goes down, you loose both
+a Sentinel and a Redis instance.
-In a minimal setup, the only required change for the slaves in `redis.conf`
-is the addition of a `slaveof` line pointing to the initial master.
-You can increase the security by defining a `requirepass` configuration in
-the master, and `masterauth` in slaves.
+The number of sentinels should ideally always be an **odd** number, for the
+consensus algorithm to be effective in the case of a failure.
----
+In a `3` nodes topology, you can only afford `1` Sentinel node going down.
+Whenever the **majority** of the Sentinels goes down, the network partition
+protection prevents destructive actions and a failover **will not be started**.
-**Configuring your own Redis server**
+Here are some examples:
-1. Add to the slaves' `redis.conf`:
+- With `5` or `6` sentinels, a maximum of `2` can go down for a failover begin.
+- With `7` sentinels, a maximum of `3` nodes can go down.
- ```conf
- # IP and port of the master Redis server
- slaveof 10.10.10.10 6379
- ```
+The **Leader** election can sometimes fail the voting round when **consensus**
+is not achieved (see the odd number of nodes requirement above). In that case,
+a new attempt will be made after the amount of time defined in
+`sentinel['failover_timeout']` (in milliseconds).
-1. Optionally, set up password authentication for increased security.
- Add the following to master's `redis.conf`:
+>**Note:**
+We will see where `sentinel['failover_timeout']` is defined later.
+
+The `failover_timeout` variable has a lot of different use cases. According to
+the official documentation:
+
+- The time needed to re-start a failover after a previous failover was
+ already tried against the same master by a given Sentinel, is two
+ times the failover timeout.
+
+- The time needed for a slave replicating to a wrong master according
+ to a Sentinel current configuration, to be forced to replicate
+ with the right master, is exactly the failover timeout (counting since
+ the moment a Sentinel detected the misconfiguration).
+
+- The time needed to cancel a failover that is already in progress but
+ did not produced any configuration change (SLAVEOF NO ONE yet not
+ acknowledged by the promoted slave).
+
+- The maximum time a failover in progress waits for all the slaves to be
+ reconfigured as slaves of the new master. However even after this time
+ the slaves will be reconfigured by the Sentinels anyway, but not with
+ the exact parallel-syncs progression as specified.
+
+### Available configuration setups
+
+Based on your infrastructure setup and how you have installed GitLab, there are
+multiple ways to configure Redis HA. Omnibus GitLab packages have Redis and/or
+Redis Sentinel bundled with them so you only need to focus on configuration.
+Pick the one that suits your needs.
+
+- [Installations from source][source]: You need to install Redis and Sentinel
+ yourself. Use the [Redis HA installation from source](redis_source.md)
+ documentation.
+- [Omnibus GitLab **Community Edition** (CE) package][ce]: Redis is bundled, so you
+ can use the package with only the Redis service enabled as described in steps
+ 1 and 2 of this document (works for both master and slave setups). To install
+ and configure Sentinel, jump directly to the Sentinel section in the
+ [Redis HA installation from source](redis_source.md#step-3-configuring-the-redis-sentinel-instances) documentation.
+- [Omnibus GitLab **Enterprise Edition** (EE) package][ee]: Both Redis and Sentinel
+ are bundled in the package, so you can use the EE package to setup the whole
+ Redis HA infrastructure (master, slave and Sentinel) which is described in
+ this document.
+- If you have installed GitLab using the Omnibus GitLab packages (CE or EE),
+ but you want to use your own external Redis server, follow steps 1-3 in the
+ [Redis HA installation from source](redis_source.md) documentation, then go
+ straight to step 4 in this guide to
+ [set up the GitLab application](#step-4-configuring-the-gitlab-application).
+
+## Configuring Redis HA
+
+This is the section where we install and setup the new Redis instances.
+
+>**Notes:**
+- We assume that you install GitLab and all HA components from scratch. If you
+ already have it installed and running, read how to
+ [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha).
+- Redis nodes (both master and slaves) will need the same password defined in
+ `redis['password']`. At any time during a failover the Sentinels can
+ reconfigure a node and change its status from master to slave and vice versa.
+
+### Prerequisites
+
+The prerequisites for a HA Redis setup are the following:
+
+1. Provision the minimum required number of instances as specified in the
+ [recommended setup](#recommended-setup) section.
+1. **Do NOT** install Redis or Redis Sentinel in the same machines your
+ GitLab application is running on. You can however opt in to install Redis
+ and Sentinel in the same machine (each in independent ones is recommended
+ though).
+1. All Redis nodes must be able to talk to each other and accept incoming
+ connections over Redis (`6379`) and Sentinel (`26379`) ports (unless you
+ change the default ones).
+1. The server that hosts the GitLab application must be able to access the
+ Redis nodes.
+1. Protect the nodes from access from external networks ([Internet][it]), using
+ firewall.
+
+### Step 1. Configuring the master Redis instance
+
+1. SSH into the **master** Redis server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
- ```conf
- # Optional password authentication for increased security
- requirepass "<password>"
+ ```ruby
+ # Enable the master role and disable all other services in the machine
+ # (you can still enable Sentinel).
+ redis_master_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.1'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
+
+ # Set up password authentication for Redis (use the same password in all nodes).
+ redis['password'] = 'redis-password-goes-here'
```
-1. Then add this line to all the slave servers' `redis.conf`:
+1. To prevent database migrations from running on upgrade, run:
- ```conf
- masterauth "<password>"
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
```
-1. Restart the Redis services for the changes to take effect.
+ Only the primary GitLab application server should handle migrations.
----
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Step 2. Configuring the slave Redis instances
-**Using Redis via Omnibus**
+1. SSH into the **slave** Redis server.
+1. [Download/install](https://about.gitlab.com/installation) the Omnibus GitLab
+ package you want using **steps 1 and 2** from the GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ and type (Community, Enterprise editions) of your current install.
+ - Do not complete any other steps on the download page.
-1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine):
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
+ # Enable the slave role and disable all other services in the machine
+ # (you can still enable Sentinel). This will also set automatically
+ # `redis['master'] = false`.
+ redis_slave_role['enable'] = true
+
+ # IP address pointing to a local IP that the other machines can reach to.
+ # You can also set bind to '0.0.0.0' which listen in all interfaces.
+ # If you really need to bind to an external accessible IP, make
+ # sure you add extra firewall rules to prevent unauthorized access.
+ redis['bind'] = '10.0.0.2'
+
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
redis['port'] = 6379
- ## Master redis instance
- redis['password'] = '<huge password string here>'
- ```
+ # The same password for Redeis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
-1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines):
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
- ```ruby
- ## Redis TCP support (will disable UNIX socket transport)
- redis['bind'] = '0.0.0.0' # or specify an IP to bind to a single one
- redis['port'] = 6379
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+ ```
+
+1. To prevent database migrations from running on upgrade, run:
- ## Slave redis instance
- redis['master_ip'] = '10.10.10.10' # IP of master Redis server
- redis['master_port'] = 6379 # Port of master Redis server
- redis['master_password'] = "<huge password string here>"
```
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
+
+ Only the primary GitLab application server should handle migrations.
-1. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure`
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
---
+These values don't have to be changed again in `/etc/gitlab/gitlab.rb` after
+a failover, as the nodes will be managed by the Sentinels, and even after a
+`gitlab-ctl reconfigure`, they will get their configuration restored by
+the same Sentinels.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. The
+following section assumes you are using Omnibus GitLab Enterprise Edition.
+For the Omnibus Community Edition and installations from source, follow the
+[Redis HA source install](redis_source.md) guide.
+
Now that the Redis servers are all set up, let's configure the Sentinel
servers.
-### Sentinel setup
+If you are not sure if your Redis servers are working and replicating
+correctly, please read the [Troubleshooting Replication](#troubleshooting-replication)
+and fix it before proceeding with Sentinel setup.
-We don't provide yet an automated way to setup and run the Sentinel daemon
-from Omnibus installation method. You must follow the instructions below and
-run it by yourself.
+You must have at least `3` Redis Sentinel servers, and they need to
+be each in an independent machine. You can configure them in the same
+machines where you've configured the other Redis servers.
-The support for Sentinel in Ruby has some [caveats](https://github.com/redis/redis-rb/issues/531).
-While you can give any name for the `master-group-name` part of the
-configuration, as in this example:
+With GitLab Enterprise Edition, you can use the Omnibus package to setup
+multiple machines with the Sentinel daemon.
-```conf
-sentinel monitor <master-group-name> <ip> <port> <quorum>
-```
+---
-,for it to work in Ruby, you have to use the "hostname" of the master Redis
-server, otherwise you will get an error message like:
-`Redis::CannotConnectError: No sentinels available.`. Read
-[Sentinel troubleshooting](#sentinel-troubleshooting) for more information.
+1. SSH into the server that will host Redis Sentinel.
+1. **You can omit this step if the Sentinels will be hosted in the same node as
+ the other Redis instances.**
-Here is an example configuration file (`sentinel.conf`) for a Sentinel node:
+ [Download/install](https://about.gitlab.com/downloads-ee) the
+ Omnibus GitLab Enterprise Edition package using **steps 1 and 2** from the
+ GitLab downloads page.
+ - Make sure you select the correct Omnibus package, with the same version
+ the GitLab application is running.
+ - Do not complete any other steps on the download page.
-```conf
-port 26379
-sentinel monitor master-redis.example.com 10.10.10.10 6379 1
-sentinel down-after-milliseconds master-redis.example.com 10000
-sentinel config-epoch master-redis.example.com 0
-sentinel leader-epoch master-redis.example.com 0
-```
+1. Edit `/etc/gitlab/gitlab.rb` and add the contents (if you are installing the
+ Sentinels in the same node as the other Redis instances, some values might
+ be duplicate below):
----
+ ```ruby
+ redis_sentinel_role['enable'] = true
-The final part is to inform the main GitLab application server of the Redis
-master and the new sentinels servers.
+ # Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
-### GitLab setup
+ # The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
-You can enable or disable sentinel support at any time in new or existing
-installations. From the GitLab application perspective, all it requires is
-the correct credentials for the master Redis and for a few Sentinel nodes.
+ # The IP of the master Redis node.
+ redis['master_ip'] = '10.0.0.1'
-It doesn't require a list of all Sentinel nodes, as in case of a failure,
-the application will need to query only one of them.
+ # Define a port so Redis can listen for TCP requests which will allow other
+ # machines to connect to it.
+ redis['port'] = 6379
->**Note:**
-The following steps should be performed in the [GitLab application server](gitlab.md).
+ # Port of master Redis server, uncomment to change to non default. Defaults
+ # to `6379`.
+ #redis['master_port'] = 6379
+
+ ## Configure Sentinel
+ sentinel['bind'] = '10.0.0.1'
+
+ # Port that Sentinel listens on, uncomment to change to non default. Defaults
+ # to `26379`.
+ # sentinel['port'] = 26379
+
+ ## Quorum must reflect the amount of voting sentinels it take to start a failover.
+ ## Value must NOT be greater then the amount of sentinels.
+ ##
+ ## The quorum can be used to tune Sentinel in two ways:
+ ## 1. If a the quorum is set to a value smaller than the majority of Sentinels
+ ## we deploy, we are basically making Sentinel more sensible to master failures,
+ ## triggering a failover as soon as even just a minority of Sentinels is no longer
+ ## able to talk with the master.
+ ## 1. If a quorum is set to a value greater than the majority of Sentinels, we are
+ ## making Sentinel able to failover only when there are a very large number (larger
+ ## than majority) of well connected Sentinels which agree about the master being down.s
+ sentinel['quorum'] = 2
+
+ ## Consider unresponsive server down after x amount of ms.
+ # sentinel['down_after_milliseconds'] = 10000
+
+ ## Specifies the failover timeout in milliseconds. It is used in many ways:
+ ##
+ ## - The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## - The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## - The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## - The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ # sentinel['failover_timeout'] = 60000
+ ```
+
+1. To prevent database migrations from running on upgrade, run:
+
+ ```
+ sudo touch /etc/gitlab/skip-auto-migrations
+ ```
-**For source based installations**
+ Only the primary GitLab application server should handle migrations.
-1. Edit `/home/git/gitlab/config/resque.yml` following the example in
- `/home/git/gitlab/config/resque.yml.example`, and uncomment the sentinels
- line, changing to the correct server credentials.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
-**For Omnibus installations**
+### Step 4. Configuring the GitLab application
+The final part is to inform the main GitLab application server of the Redis
+Sentinels servers and authentication credentials.
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of the listed.
+
+>**Note:**
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels on it for a HA setup.
+
+1. SSH into the server where the GitLab application is installed.
1. Edit `/etc/gitlab/gitlab.rb` and add/change the following lines:
- ```ruby
- gitlab-rails['redis_host'] = "master-redis.example.com"
- gitlab-rails['redis_port'] = 6379
- gitlab-rails['redis_password'] = '<huge password string here>'
- gitlab-rails['redis_sentinels'] = [
- {'host' => '10.10.10.1', 'port' => 26379},
- {'host' => '10.10.10.2', 'port' => 26379},
- {'host' => '10.10.10.3', 'port' => 26379}
+ ```
+ ## Must be the same in every sentinel node
+ redis['master_name'] = 'gitlab-redis'
+
+ ## The same password for Redis authentication you set up for the master node.
+ redis['password'] = 'redis-password-goes-here'
+
+ ## A list of sentinels with `host` and `port`
+ gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
]
```
-1. [Reconfigure] the GitLab for the changes to take effect.
+1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
-### Sentinel troubleshooting
+## Switching from an existing single-machine installation to Redis HA
-If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
-there may be something wrong with your configuration files or it can be related
-to [this issue][gh-531] ([pull request][gh-534] that should make things better).
+If you already have a single-machine GitLab install running, you will need to
+replicate from this machine first, before de-activating the Redis instance
+inside it.
+
+Your single-machine install will be the initial **Master**, and the `3` others
+should be configured as **Slave** pointing to this machine.
-It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`,
-otherwise `redis-rb` will not work properly.
+After replication catches up, you will need to stop services in the
+single-machine install, to rotate the **Master** to one of the new nodes.
-The hostname ('my-primary-redis') of the primary Redis server (`sentinel.conf`)
-**must** match the one configured in GitLab (`resque.yml` for source installations
-or `gitlab-rails['redis_*']` in Omnibus) and it must be valid ex:
+Make the required changes in configuration and restart the new nodes again.
-```conf
-# sentinel.conf:
-sentinel monitor my-primary-redis 10.10.10.10 6379 1
-sentinel down-after-milliseconds my-primary-redis 10000
-sentinel config-epoch my-primary-redis 0
-sentinel leader-epoch my-primary-redis 0
+To disable redis in the single install, edit `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis['enable'] = false
+```
+
+If you fail to replicate first, you may loose data (unprocessed background jobs).
+
+## Example of a minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+>**Note:**
+Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. For
+different setups, read the
+[available configuration setups](#available-configuration-setups) section.
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines and block traffic from the
+outside (Internet).
+
+We will use the same `3` nodes with **Redis** + **Sentinel** topology
+discussed in [Redis setup overview](#redis-setup-overview) and
+[Sentinel setup overview](#sentinel-setup-overview) documentation.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_master_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.1'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+redis['master_password'] = 'redis-password-goes-here' # the same value defined in redis['password'] in the master instance
+redis['master_ip'] = '10.0.0.1' # ip of the initial master redis instance
+#redis['master_port'] = 6379 # port of the initial master redis instance, uncomment to change to non default
+sentinel['bind'] = '10.0.0.1'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.2'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.2'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis_slave_role['enable'] = true
+redis_sentinel_role['enable'] = true
+redis['bind'] = '10.0.0.3'
+redis['port'] = 6379
+redis['password'] = 'redis-password-goes-here'
+redis['master_password'] = 'redis-password-goes-here'
+redis['master_ip'] = '10.0.0.1' # IP of master Redis server
+#redis['master_port'] = 6379 # Port of master Redis server, uncomment to change to non default
+redis['master_name'] = 'gitlab-redis' # must be the same in every sentinel node
+sentinel['bind'] = '10.0.0.3'
+# sentinel['port'] = 26379 # uncomment to change default port
+sentinel['quorum'] = 2
+# sentinel['down_after_milliseconds'] = 10000
+# sentinel['failover_timeout'] = 60000
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+### Example configuration for the GitLab application
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+redis['master_name'] = 'gitlab-redis'
+redis['password'] = 'redis-password-goes-here'
+gitlab_rails['redis_sentinels'] = [
+ {'host' => '10.0.0.1', 'port' => 26379},
+ {'host' => '10.0.0.2', 'port' => 26379},
+ {'host' => '10.0.0.3', 'port' => 26379}
+]
+```
+
+[Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
+
+## Advanced configuration
+
+Omnibus GitLab configures some things behind the curtains to make the sysadmins'
+lives easier. If you want to know what happens underneath keep reading.
+
+### Control running services
+
+In the previous example, we've used `redis_sentinel_role` and
+`redis_master_role` which simplifies the amount of configuration changes.
+
+If you want more control, here is what each one sets for you automatically
+when enabled:
+
+```ruby
+## Redis Sentinel Role
+redis_sentinel_role['enable'] = true
+
+# When Sentinel Role is enabled, the following services are also enabled
+sentinel['enable'] = true
+
+# The following services are disabled
+redis['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+-------
+
+## Redis master/slave Role
+redis_master_role['enable'] = true # enable only one of them
+redis_slave_role['enable'] = true # enable only one of them
+
+# When Redis Master or Slave role are enabled, the following services are
+# enabled/disabled. Note that if Redis and Sentinel roles are combined, both
+# services will be enabled.
+
+# The following services are disabled
+sentinel['enable'] = false
+bootstrap['enable'] = false
+nginx['enable'] = false
+postgresql['enable'] = false
+gitlab_rails['enable'] = false
+mailroom['enable'] = false
+
+# For Redis Slave role, also change this setting from default 'true' to 'false':
+redis['master'] = false
```
-```yaml
-# resque.yaml
-production:
- url: redis://my-primary-redis:6378
- sentinels:
- -
- host: slave1
- port: 26380 # point to sentinel, not to redis port
- -
- host: slave2
- port: 26381 # point to sentinel, not to redis port
+You can find the relevant attributes defined in [gitlab_rails.rb][omnifile].
+
+## Troubleshooting
+
+There are a lot of moving parts that needs to be taken care carefully
+in order for the HA setup to work as expected.
+
+Before proceeding with the troubleshooting below, check your firewall rules:
+
+- Redis machines
+ - Accept TCP connection in `6379`
+ - Connect to the other Redis machines via TCP in `6379`
+- Sentinel machines
+ - Accept TCP connection in `26379`
+ - Connect to other Sentinel machines via TCP in `26379`
+ - Connect to the Redis machines via TCP in `6379`
+
+### Troubleshooting Redis replication
+
+You can check if everything is correct by connecting to each server using
+`redis-cli` application, and sending the `INFO` command.
+
+If authentication was correctly defined, it should fail with:
+`NOAUTH Authentication required` error. Try to authenticate with the
+previous defined password with `AUTH redis-password-goes-here` and
+try the `INFO` command again.
+
+Look for the `# Replication` section where you should see some important
+information like the `role` of the server.
+
+When connected to a `master` redis, you will see the number of connected
+`slaves`, and a list of each with connection details:
+
+```
+# Replication
+role:master
+connected_slaves:1
+slave0:ip=10.133.5.21,port=6379,state=online,offset=208037514,lag=1
+master_repl_offset:208037658
+repl_backlog_active:1
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:206989083
+repl_backlog_histlen:1048576
```
-When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel)
+When it's a `slave`, you will see details of the master connection and if
+its `up` or `down`:
+
+```
+# Replication
+role:slave
+master_host:10.133.1.58
+master_port:6379
+master_link_status:up
+master_last_io_seconds_ago:1
+master_sync_in_progress:0
+slave_repl_offset:208096498
+slave_priority:100
+slave_read_only:1
+connected_slaves:0
+master_repl_offset:0
+repl_backlog_active:0
+repl_backlog_size:1048576
+repl_backlog_first_byte_offset:0
+repl_backlog_histlen:0
+```
+
+### Troubleshooting Sentinel
+
+If you get an error like: `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this issue][gh-531].
+
+You must make sure you are defining the same value in `redis['master_name']`
+and `redis['master_pasword']` as you defined for your sentinel node.
+
+The way the redis connector `redis-rb` works with sentinel is a bit
+non-intuitive. We try to hide the complexity in omnibus, but it still requires
+a few extra configs.
---
@@ -273,7 +788,7 @@ To make sure your configuration is correct:
sudo gitlab-rails console
# For source installations
- sudo -u git rails console RAILS_ENV=production
+ sudo -u git rails console production
```
1. Run in the console:
@@ -288,8 +803,8 @@ To make sure your configuration is correct:
1. To simulate a failover on master Redis, SSH into the Redis server and run:
```bash
- # port must match your master redis port
- redis-cli -h localhost -p 6379 DEBUG sleep 60
+ # port must match your master redis port, and the sleep time must be a few seconds bigger than defined one
+ redis-cli -h localhost -p 6379 DEBUG sleep 20
```
1. Then back in the Rails console from the first step, run:
@@ -301,10 +816,26 @@ To make sure your configuration is correct:
You should see a different port after a few seconds delay
(the failover/reconnect time).
----
-Read more on high-availability configuration:
+## Changelog
+
+Changes to Redis HA over time.
+
+**8.14**
+
+- Redis Sentinel support is production-ready and bundled in the Omnibus GitLab
+ Enterprise Edition package
+- Documentation restructure for better readability
+
+**8.11**
+
+- Experimental Redis Sentinel support was added
+
+## Further reading
+
+Read more on High Availability:
+1. [High Availability Overview](README.md)
1. [Configure the database](database.md)
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
@@ -315,3 +846,10 @@ Read more on high-availability configuration:
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[gh-531]: https://github.com/redis/redis-rb/issues/531
[gh-534]: https://github.com/redis/redis-rb/issues/534
+[redis]: http://redis.io/
+[sentinel]: http://redis.io/topics/sentinel
+[omnifile]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb
+[source]: ../../install/installation.md
+[ce]: https://about.gitlab.com/downloads
+[ee]: https://about.gitlab.com/downloads-ee
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
new file mode 100644
index 00000000000..3629772b8af
--- /dev/null
+++ b/doc/administration/high_availability/redis_source.md
@@ -0,0 +1,366 @@
+# Configuring non-Omnibus Redis for GitLab HA
+
+This is the documentation for configuring a Highly Available Redis setup when
+you have installed Redis all by yourself and not using the bundled one that
+comes with the Omnibus packages.
+
+We cannot stress enough the importance of reading the
+[Overview section](redis.md#overview) of the Omnibus Redis HA as it provides
+some invaluable information to the configuration of Redis. Please proceed to
+read it before going forward with this guide.
+
+We also highly recommend that you use the Omnibus GitLab packages, as we
+optimize them specifically for GitLab, and we will take care of upgrading Redis
+to the latest supported version.
+
+If you're not sure whether this guide is for you, please refer to
+[Available configuration setups](redis.md#available-configuration-setups) in
+the Omnibus Redis HA documentation.
+
+## Configuring your own Redis server
+
+This is the section where we install and setup the new Redis instances.
+
+### Prerequisites
+
+- All Redis servers in this guide must be configured to use a TCP connection
+ instead of a socket. To configure Redis to use TCP connections you need to
+ define both `bind` and `port` in the Redis config file. You can bind to all
+ interfaces (`0.0.0.0`) or specify the IP of the desired interface
+ (e.g., one from an internal network).
+- Since Redis 3.2, you must define a password to receive external connections
+ (`requirepass`).
+- If you are using Redis with Sentinel, you will also need to define the same
+ password for the slave password definition (`masterauth`) in the same instance.
+
+In addition, read the prerequisites as described in the
+[Omnibus Redis HA document](redis.md#prerequisites) since they provide some
+valuable information for the general setup.
+
+### Step 1. Configuring the master Redis instance
+
+Assuming that the Redis master instance IP is `10.0.0.1`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Step 2. Configuring the slave Redis instances
+
+Assuming that the Redis slave instance IP is `10.0.0.2`:
+
+1. [Install Redis](../../install/installation.md#6-redis)
+1. Edit `/etc/redis/redis.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.2
+
+ ## Define a `port` to force redis to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 6379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define `slaveof` pointing to the Redis master instance with IP and port.
+ slaveof 10.0.0.1 6379
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other slave nodes.
+
+### Step 3. Configuring the Redis Sentinel instances
+
+Sentinel is a special type of Redis server. It inherits most of the basic
+configuration options you can define in `redis.conf`, with specific ones
+starting with `sentinel` prefix.
+
+Assuming that the Redis Sentinel is installed on the same instance as Redis
+master with IP `10.0.0.1` (some settings might overlap with the master):
+
+1. [Install Redis Sentinel](http://redis.io/topics/sentinel)
+1. Edit `/etc/redis/sentinel.conf`:
+
+ ```conf
+ ## Define a `bind` address pointing to a local IP that your other machines
+ ## can reach you. If you really need to bind to an external accessible IP, make
+ ## sure you add extra firewall rules to prevent unauthorized access:
+ bind 10.0.0.1
+
+ ## Define a `port` to force Sentinel to listen on TCP so other machines can
+ ## connect to it (default port is `6379`).
+ port 26379
+
+ ## Set up password authentication (use the same password in all nodes).
+ ## The password should be defined equal for both `requirepass` and `masterauth`
+ ## when setting up Redis to use with Sentinel.
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+
+ ## Define with `sentinel auth-pass` the same shared password you have
+ ## defined for both Redis master and slaves instances.
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+
+ ## Define with `sentinel monitor` the IP and port of the Redis
+ ## master node, and the quorum required to start a failover.
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+
+ ## Define with `sentinel down-after-milliseconds` the time in `ms`
+ ## that an unresponsive server will be considered down.
+ sentinel down-after-milliseconds gitlab-redis 10000
+
+ ## Define a value for `sentinel failover_timeout` in `ms`. This has multiple
+ ## meanings:
+ ##
+ ## * The time needed to re-start a failover after a previous failover was
+ ## already tried against the same master by a given Sentinel, is two
+ ## times the failover timeout.
+ ##
+ ## * The time needed for a slave replicating to a wrong master according
+ ## to a Sentinel current configuration, to be forced to replicate
+ ## with the right master, is exactly the failover timeout (counting since
+ ## the moment a Sentinel detected the misconfiguration).
+ ##
+ ## * The time needed to cancel a failover that is already in progress but
+ ## did not produced any configuration change (SLAVEOF NO ONE yet not
+ ## acknowledged by the promoted slave).
+ ##
+ ## * The maximum time a failover in progress waits for all the slaves to be
+ ## reconfigured as slaves of the new master. However even after this time
+ ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## the exact parallel-syncs progression as specified.
+ sentinel failover_timeout 30000
+ ```
+1. Restart the Redis service for the changes to take effect.
+1. Go through the steps again for all the other Sentinel nodes.
+
+### Step 4. Configuring the GitLab application
+
+You can enable or disable Sentinel support at any time in new or existing
+installations. From the GitLab application perspective, all it requires is
+the correct credentials for the Sentinel nodes.
+
+While it doesn't require a list of all Sentinel nodes, in case of a failure,
+it needs to access at least one of listed ones.
+
+The following steps should be performed in the [GitLab application server](gitlab.md)
+which ideally should not have Redis or Sentinels in the same machine for a HA
+setup:
+
+1. Edit `/home/git/gitlab/config/resque.yml` following the example in
+ [resque.yml.example][resque], and uncomment the Sentinel lines, pointing to
+ the correct server credentials:
+
+ ```yaml
+ # resque.yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Example of minimal configuration with 1 master, 2 slaves and 3 Sentinels
+
+In this example we consider that all servers have an internal network
+interface with IPs in the `10.0.0.x` range, and that they can connect
+to each other using these IPs.
+
+In a real world usage, you would also setup firewall rules to prevent
+unauthorized access from other machines, and block traffic from the
+outside ([Internet][it]).
+
+For this example, **Sentinel 1** will be configured in the same machine as the
+**Redis Master**, **Sentinel 2** and **Sentinel 3** in the same machines as the
+**Slave 1** and **Slave 2** respectively.
+
+Here is a list and description of each **machine** and the assigned **IP**:
+
+* `10.0.0.1`: Redis Master + Sentinel 1
+* `10.0.0.2`: Redis Slave 1 + Sentinel 2
+* `10.0.0.3`: Redis Slave 2 + Sentinel 3
+* `10.0.0.4`: GitLab application
+
+Please note that after the initial configuration, if a failover is initiated
+by the Sentinel nodes, the Redis nodes will be reconfigured and the **Master**
+will change permanently (including in `redis.conf`) from one node to the other,
+until a new failover is initiated again.
+
+The same thing will happen with `sentinel.conf` that will be overridden after the
+initial execution, after any new sentinel node starts watching the **Master**,
+or a failover promotes a different **Master** node.
+
+### Example configuration for Redis master and Sentinel 1
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.1
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 1 and Sentinel 2
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.2
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration for Redis slave 2 and Sentinel 3
+
+1. In `/etc/redis/redis.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 6379
+ requirepass redis-password-goes-here
+ masterauth redis-password-goes-here
+ slaveof 10.0.0.1 6379
+ ```
+
+1. In `/etc/redis/sentinel.conf`:
+
+ ```conf
+ bind 10.0.0.3
+ port 26379
+ sentinel auth-pass gitlab-redis redis-password-goes-here
+ sentinel monitor gitlab-redis 10.0.0.1 6379 2
+ sentinel down-after-milliseconds gitlab-redis 10000
+ sentinel failover_timeout 30000
+ ```
+
+1. Restart the Redis service for the changes to take effect.
+
+### Example configuration of the GitLab application
+
+1. Edit `/home/git/gitlab/config/resque.yml`:
+
+ ```yaml
+ production:
+ url: redis://:redi-password-goes-here@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+ ```
+
+1. [Restart GitLab][restart] for the changes to take effect.
+
+## Troubleshooting
+
+We have a more detailed [Troubleshooting](redis.md#troubleshooting) explained
+in the documentation for Omnibus GitLab installations. Here we will list only
+the things that are specific to a source installation.
+
+If you get an error in GitLab like `Redis::CannotConnectError: No sentinels available.`,
+there may be something wrong with your configuration files or it can be related
+to [this upstream issue][gh-531].
+
+You must make sure that `resque.yml` and `sentinel.conf` are configured correctly,
+otherwise `redis-rb` will not work properly.
+
+The `master-group-name` ('gitlab-redis') defined in (`sentinel.conf`)
+**must** be used as the hostname in GitLab (`resque.yml`):
+
+```conf
+# sentinel.conf:
+sentinel monitor gitlab-redis 10.0.0.1 6379 2
+sentinel down-after-milliseconds gitlab-redis 10000
+sentinel config-epoch gitlab-redis 0
+sentinel leader-epoch gitlab-redis 0
+```
+
+```yaml
+# resque.yaml
+production:
+ url: redis://:myredispassword@gitlab-redis/
+ sentinels:
+ -
+ host: 10.0.0.1
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.2
+ port: 26379 # point to sentinel, not to redis port
+ -
+ host: 10.0.0.3
+ port: 26379 # point to sentinel, not to redis port
+```
+
+When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel).
+
+[gh-531]: https://github.com/redis/redis-rb/issues/531
+[downloads]: https://about.gitlab.com/downloads
+[restart]: ../restart_gitlab.md#installations-from-source
+[it]: https://gitlab.com/gitlab-org/gitlab-ce/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png
diff --git a/doc/raketasks/check_repos_output.png b/doc/administration/img/raketasks/check_repos_output.png
index 1f632566b00..1f632566b00 100644
--- a/doc/raketasks/check_repos_output.png
+++ b/doc/administration/img/raketasks/check_repos_output.png
Binary files differ
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
index 4b582d16b64..0daceb98d99 100644
--- a/doc/administration/operations.md
+++ b/doc/administration/operations.md
@@ -1,6 +1,7 @@
# GitLab operations
- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
+- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
- [Moving repositories to a new location](operations/moving_repositories.md)
diff --git a/doc/administration/operations/img/sidekiq_job_throttling.png b/doc/administration/operations/img/sidekiq_job_throttling.png
new file mode 100644
index 00000000000..7f29a4d3c46
--- /dev/null
+++ b/doc/administration/operations/img/sidekiq_job_throttling.png
Binary files differ
diff --git a/doc/administration/operations/sidekiq_job_throttling.md b/doc/administration/operations/sidekiq_job_throttling.md
new file mode 100644
index 00000000000..ddeaa22e288
--- /dev/null
+++ b/doc/administration/operations/sidekiq_job_throttling.md
@@ -0,0 +1,33 @@
+# Sidekiq Job throttling
+
+> Note: Introduced with GitLab 8.14
+
+When your GitLab installation needs to handle tens of thousands of background
+jobs, it can be convenient to throttle queues that do not need to be executed
+immediately, e.g. long running jobs like Pipelines, thus allowing jobs that do
+need to be executed immediately to have access to more resources.
+
+In order to accomplish this, you can limit the amount of workers that certain
+slow running queues can have available. This is what we call Sidekiq Job
+Throttling. Depending on your infrastructure, you might have different slow
+running queues, which is why you can choose which queues you want to throttle
+and by how much you want to throttle them.
+
+These settings are available in the Application Settings of your GitLab
+installation.
+
+![Sidekiq Job Throttling](img/sidekiq_job_throttling.png)
+
+The throttle factor determines the maximum number of workers a queue can run on.
+This value gets multiplied by `:concurrency` value set in the Sidekiq settings
+and rounded up to the closest full integer.
+
+So, for example, you set the `:concurrency` to 25 and the `Throttling factor` to
+0.1, the maximum workers assigned to the selected queues would be 3.
+
+```ruby
+queue_limit = (factor * Sidekiq.options[:concurrency]).ceil
+```
+
+After enabling the job throttling, you will need to restart your GitLab
+instance, in order for the changes to take effect. \ No newline at end of file
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
new file mode 100644
index 00000000000..d1d2fed4861
--- /dev/null
+++ b/doc/administration/raketasks/check.md
@@ -0,0 +1,97 @@
+# Check Rake Tasks
+
+## Repository Integrity
+
+Even though Git is very resilient and tries to prevent data integrity issues,
+there are times when things go wrong. The following Rake tasks intend to
+help GitLab administrators diagnose problem repositories so they can be fixed.
+
+There are 3 things that are checked to determine integrity.
+
+1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
+ This step verifies the connectivity and validity of objects in the repository.
+1. Check for `config.lock` in the repository directory.
+1. Check for any branch/references lock files in `refs/heads`.
+
+It's important to note that the existence of `config.lock` or reference locks
+alone do not necessarily indicate a problem. Lock files are routinely created
+and removed as Git and GitLab perform operations on the repository. They serve
+to prevent data integrity issues. However, if a Git operation is interrupted these
+locks may not be cleaned up properly.
+
+The following symptoms may indicate a problem with repository integrity. If users
+experience these symptoms you may use the rake tasks described below to determine
+exactly which repositories are causing the trouble.
+
+- Receiving an error when trying to push code - `remote: error: cannot lock ref`
+- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
+
+### Check all GitLab repositories
+
+This task loops through all repositories on the GitLab server and runs the
+3 integrity checks described previously.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:repo:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:repo:check RAILS_ENV=production
+```
+
+### Check repositories for a specific user
+
+This task checks all repositories that a specific user has access to. This is important
+because sometimes you know which user is experiencing trouble but you don't know
+which project might be the cause.
+
+If the rake task is executed without brackets at the end, you will be prompted
+to enter a username.
+
+**Omnibus Installation**
+
+```bash
+sudo gitlab-rake gitlab:user:check_repos
+sudo gitlab-rake gitlab:user:check_repos[<username>]
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:user:check_repos RAILS_ENV=production
+sudo -u git -H bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
+```
+
+Example output:
+
+![gitlab:user:check_repos output](../img/raketasks/check_repos_output.png)
+
+## LDAP Check
+
+The LDAP check Rake task will test the bind_dn and password credentials
+(if configured) and will list a sample of LDAP users. This task is also
+executed as part of the `gitlab:check` task, but can run independently
+using the command below.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:ldap:check
+```
+
+**Source Installation**
+
+```bash
+sudo -u git -H bundle exec rake gitlab:ldap:check RAILS_ENV=production
+```
+
+By default, the task will return a sample of 100 LDAP users. Change this
+limit by passing a number to the check task:
+
+```bash
+rake gitlab:ldap:check[50]
+```
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 5a9a1582877..14cd7a03826 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -105,6 +105,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
```
```ruby
@@ -133,6 +135,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
```
1. Reconfigure GitLab and restart mailroom for the changes to take effect:
@@ -192,6 +196,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
```yaml
@@ -221,6 +227,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
@@ -277,6 +285,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
```
As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 0b5f7778fc7..f68eeb9f86b 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -240,3 +240,21 @@ Example response:
"branch_name": "newbranch"
}
```
+
+## Delete merged branches
+
+Will delete all branches that are merged into the project's default branch.
+
+```
+DELETE /projects/:id/repository/merged_branches
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+It returns `200` to indicate deletion of all merged branches was started.
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/merged_branches"
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index 0476cac0eda..bca2f9e44ef 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -45,7 +45,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -89,7 +89,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -163,7 +163,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -193,7 +193,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
@@ -260,7 +260,7 @@ Example of response
"ref": "master",
"sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
"status": "pending"
- }
+ },
"ref": "master",
"runner": null,
"stage": "test",
diff --git a/doc/api/groups.md b/doc/api/groups.md
index b56d74d25e0..5e6f498c365 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -6,8 +6,13 @@ Get a list of groups. (As user: my groups or all available, as admin: all groups
Parameters:
-- `all_available` (optional) - if passed, show all groups you have access to
-- `skip_groups` (optional)(array of group IDs) - if passed, skip groups
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `skip_groups` | array of integers | no | Skip the group IDs passes |
+| `all_available` | boolean | no | Show all the groups you have access to |
+| `search` | string | no | Return list of authorized groups matching the search criteria |
+| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
+| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
```
GET /groups
@@ -26,6 +31,15 @@ GET /groups
You can search for groups by name or path, see below.
+=======
+## List owned groups
+
+Get a list of groups which are owned by the authenticated user.
+
+```
+GET /groups/owned
+```
+
## List a group's projects
Get a list of projects in this group.
diff --git a/doc/api/labels.md b/doc/api/labels.md
index b5242037949..78686fdcad4 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -20,56 +20,61 @@ Example response:
```json
[
- {
- "name" : "bug",
- "color" : "#d9534f",
- "description": "Bug reported by user",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": 10
- },
- {
- "color" : "#d9534f",
- "name" : "confirmed",
- "description": "Confirmed issue",
- "open_issues_count": 2,
- "closed_issues_count": 5,
- "open_merge_requests_count": 0,
- "subscribed": false,
- "priority": null
- },
- {
- "name" : "critical",
- "color" : "#d9534f",
- "description": "Critical issue. Need fix ASAP",
- "open_issues_count": 1,
- "closed_issues_count": 3,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
- },
- {
- "name" : "documentation",
- "color" : "#f0ad4e",
- "description": "Issue about documentation",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 2,
- "subscribed": false,
- "priority": null
- },
- {
- "color" : "#5cb85c",
- "name" : "enhancement",
- "description": "Enhancement proposal",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "subscribed": false,
- "priority": null
- }
+ {
+ "id" : 1,
+ "name" : "bug",
+ "color" : "#d9534f",
+ "description": "Bug reported by user",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 1,
+ "subscribed": false,
+ "priority": 10
+ },
+ {
+ "id" : 4,
+ "color" : "#d9534f",
+ "name" : "confirmed",
+ "description": "Confirmed issue",
+ "open_issues_count": 2,
+ "closed_issues_count": 5,
+ "open_merge_requests_count": 0,
+ "subscribed": false,
+ "priority": null
+ },
+ {
+ "id" : 7,
+ "name" : "critical",
+ "color" : "#d9534f",
+ "description": "Critical issue. Need fix ASAP",
+ "open_issues_count": 1,
+ "closed_issues_count": 3,
+ "open_merge_requests_count": 1,
+ "subscribed": false,
+ "priority": null
+ },
+ {
+ "id" : 8,
+ "name" : "documentation",
+ "color" : "#f0ad4e",
+ "description": "Issue about documentation",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 2,
+ "subscribed": false,
+ "priority": null
+ },
+ {
+ "id" : 9,
+ "color" : "#5cb85c",
+ "name" : "enhancement",
+ "description": "Enhancement proposal",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 1,
+ "subscribed": true,
+ "priority": null
+ }
]
```
@@ -100,13 +105,15 @@ Example response:
```json
{
- "name" : "feature",
- "color" : "#5843AD",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "description": null,
- "priority": null
+ "id" : 10,
+ "name" : "feature",
+ "color" : "#5843AD",
+ "description":null,
+ "open_issues_count": 0,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 0,
+ "subscribed": false,
+ "priority": null
}
```
@@ -135,15 +142,15 @@ Example response:
```json
{
- "title" : "feature",
- "color" : "#5843AD",
- "description": "New feature proposal",
- "updated_at" : "2015-11-03T21:22:30.737Z",
- "template" : false,
- "project_id" : 1,
- "created_at" : "2015-11-03T21:22:30.737Z",
- "id" : 9,
- "priority": null
+ "id" : 1,
+ "name" : "bug",
+ "color" : "#d9534f",
+ "description": "Bug reported by user",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 1,
+ "subscribed": false,
+ "priority": null
}
```
@@ -178,13 +185,15 @@ Example response:
```json
{
- "color" : "#8E44AD",
- "name" : "docs",
- "description": "Documentation",
- "open_issues_count": 1,
- "closed_issues_count": 0,
- "open_merge_requests_count": 1,
- "priority": null
+ "id" : 8,
+ "name" : "docs",
+ "color" : "#8E44AD",
+ "description": "Documentation",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 2,
+ "subscribed": false,
+ "priority": null
}
```
@@ -213,14 +222,15 @@ Example response:
```json
{
- "name": "Docs",
- "color": "#cc0033",
- "description": "",
- "open_issues_count": 0,
- "closed_issues_count": 0,
- "open_merge_requests_count": 0,
- "subscribed": true,
- "priority": null
+ "id" : 1,
+ "name" : "bug",
+ "color" : "#d9534f",
+ "description": "Bug reported by user",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 1,
+ "subscribed": true,
+ "priority": null
}
```
@@ -249,13 +259,14 @@ Example response:
```json
{
- "name": "Docs",
- "color": "#cc0033",
- "description": "",
- "open_issues_count": 0,
- "closed_issues_count": 0,
- "open_merge_requests_count": 0,
- "subscribed": false,
- "priority": null
+ "id" : 1,
+ "name" : "bug",
+ "color" : "#d9534f",
+ "description": "Bug reported by user",
+ "open_issues_count": 1,
+ "closed_issues_count": 0,
+ "open_merge_requests_count": 1,
+ "subscribed": false,
+ "priority": null
}
```
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index ff6c9e4931c..aea1c12a392 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -4,7 +4,7 @@
**Valid notification levels**
-The notification levels are defined in the `NotificationSetting::level` model enumeration. Currently, these levels are recognized:
+The notification levels are defined in the `NotificationSetting.level` model enumeration. Currently, these levels are recognized:
```
disabled
@@ -28,6 +28,8 @@ reopen_merge_request
close_merge_request
reassign_merge_request
merge_merge_request
+failed_pipeline
+success_pipeline
```
## Global notification settings
@@ -77,6 +79,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
Example response:
@@ -141,6 +145,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
| `merge_merge_request` | boolean | no | Enable/disable this notification |
+| `failed_pipeline` | boolean | no | Enable/disable this notification |
+| `success_pipeline` | boolean | no | Enable/disable this notification |
Example responses:
@@ -161,7 +167,9 @@ Example responses:
"reopen_merge_request": false,
"close_merge_request": false,
"reassign_merge_request": false,
- "merge_merge_request": false
+ "merge_merge_request": false,
+ "failed_pipeline": false,
+ "success_pipeline": false
}
}
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index a29b3eb6f44..6455c333faf 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -114,6 +114,51 @@ Example of response
}
```
+## Create a new pipeline
+
+> [Introduced][ce-7209] in GitLab 8.14
+
+```
+POST /projects/:id/pipeline
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `ref` | string | yes | Reference to commit |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master"
+```
+
+Example of response
+
+```json
+{
+ "id": 61,
+ "sha": "384c444e840a515b23f21915ee5766b87068a70d",
+ "ref": "master",
+ "status": "pending",
+ "before_sha": "0000000000000000000000000000000000000000",
+ "tag": false,
+ "yaml_errors": null,
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2016-11-04T09:36:13.747Z",
+ "updated_at": "2016-11-04T09:36:13.977Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null
+}
+```
+
## Retry failed builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11
@@ -205,3 +250,4 @@ Response:
```
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
+[ce-7209]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7209
diff --git a/doc/api/projects.md b/doc/api/projects.md
index bbb3bfb4995..467a880ac13 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -850,7 +850,7 @@ POST /projects/:id/archive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/archive"
```
Example response:
@@ -939,7 +939,7 @@ POST /projects/:id/unarchive
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unarchive"
```
Example response:
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index b6cca5d4e2a..bcf8b955044 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -13,44 +13,58 @@ Parameters:
- `id` (required) - The ID of a project
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
```json
[
{
- "name": "assets",
+ "id": "a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba",
+ "name": "html",
"type": "tree",
- "mode": "040000",
- "id": "6229c43a7e16fcc7e95f923f8ddadb8281d9c6c6"
+ "path": "files/html",
+ "mode": "040000"
},
{
- "name": "contexts",
+ "id": "4535904260b1082e14f867f7a24fd8c21495bde3",
+ "name": "images",
"type": "tree",
- "mode": "040000",
- "id": "faf1cdf33feadc7973118ca42d35f1e62977e91f"
+ "path": "files/images",
+ "mode": "040000"
},
{
- "name": "controllers",
+ "id": "31405c5ddef582c5a9b7a85230413ff90e2fe720",
+ "name": "js",
"type": "tree",
- "mode": "040000",
- "id": "95633e8d258bf3dfba3a5268fb8440d263218d74"
+ "path": "files/js",
+ "mode": "040000"
},
{
- "name": "Rakefile",
- "type": "blob",
- "mode": "100644",
- "id": "35b2f05cbb4566b71b34554cf184a9d0bd9d46d6"
+ "id": "cc71111cfad871212dc99572599a568bfe1e7e00",
+ "name": "lfs",
+ "type": "tree",
+ "path": "files/lfs",
+ "mode": "040000"
},
{
- "name": "VERSION",
- "type": "blob",
- "mode": "100644",
- "id": "803e4a4f3727286c3093c63870c2b6524d30ec4f"
+ "id": "fd581c619bf59cfdfa9c8282377bb09c2f897520",
+ "name": "markdown",
+ "type": "tree",
+ "path": "files/markdown",
+ "mode": "040000"
+ },
+ {
+ "id": "23ea4d11a4bdd960ee5320c5cb65b5b3fdbc60db",
+ "name": "ruby",
+ "type": "tree",
+ "path": "files/ruby",
+ "mode": "040000"
},
{
- "name": "config.ru",
+ "id": "7d70e02340bac451f281cecf0a980907974bd8be",
+ "name": "whitespace",
"type": "blob",
- "mode": "100644",
- "id": "dfd2d862237323aa599be31b473d70a8a817943b"
+ "path": "files/whitespace",
+ "mode": "100644"
}
]
```
diff --git a/doc/api/users.md b/doc/api/users.md
index a50ba5432fe..b38c335490a 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -33,6 +33,18 @@ GET /users
]
```
+In addition, you can filter users based on states eg. `blocked`, `active`
+This works only to filter users who are `blocked` or `active`.
+It does not support `active=false` or `blocked=false`.
+
+```
+GET /users?active=true
+```
+
+```
+GET /users?blocked=true
+```
+
### For admins
```
@@ -120,6 +132,8 @@ For example:
GET /users?username=jack_smith
```
+You can search for users who are external with: `/users?external=true`
+
## Single user
Get a single user.
@@ -355,24 +369,24 @@ Parameters:
Get a list of a specified user's SSH keys. Available only for admin
```
-GET /users/:uid/keys
+GET /users/:id/keys
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
## Single SSH key
Get a single key.
```
-GET /user/keys/:id
+GET /user/keys/:key_id
```
Parameters:
-- `id` (required) - The ID of an SSH key
+- `key_id` (required) - The ID of an SSH key
```json
{
@@ -444,25 +458,25 @@ This is an idempotent function and calling it on a key that is already deleted
or not available results in `200 OK`.
```
-DELETE /user/keys/:id
+DELETE /user/keys/:key_id
```
Parameters:
-- `id` (required) - SSH key ID
+- `key_id` (required) - SSH key ID
## Delete SSH key for given user
Deletes key owned by a specified user. Available only for admin.
```
-DELETE /users/:uid/keys/:id
+DELETE /users/:id/keys/:key_id
```
Parameters:
-- `uid` (required) - id of specified user
-- `id` (required) - SSH key ID
+- `id` (required) - id of specified user
+- `key_id` (required) - SSH key ID
Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
@@ -496,24 +510,24 @@ Parameters:
Get a list of a specified user's emails. Available only for admin
```
-GET /users/:uid/emails
+GET /users/:id/emails
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
## Single email
Get a single email.
```
-GET /user/emails/:id
+GET /user/emails/:email_id
```
Parameters:
-- `id` (required) - email ID
+- `email_id` (required) - email ID
```json
{
@@ -576,25 +590,25 @@ This is an idempotent function and calling it on a email that is already deleted
or not available results in `200 OK`.
```
-DELETE /user/emails/:id
+DELETE /user/emails/:email_id
```
Parameters:
-- `id` (required) - email ID
+- `email_id` (required) - email ID
## Delete email for given user
Deletes email owned by a specified user. Available only for admin.
```
-DELETE /users/:uid/emails/:id
+DELETE /users/:id/emails/:email_id
```
Parameters:
-- `uid` (required) - id of specified user
-- `id` (required) - email ID
+- `id` (required) - id of specified user
+- `email_id` (required) - email ID
Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
@@ -603,12 +617,12 @@ Will return `200 OK` on success, or `404 Not found` if either user or email cann
Blocks the specified user. Available only for admin.
```
-PUT /users/:uid/block
+PUT /users/:id/block
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
@@ -618,12 +632,12 @@ Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
Unblocks the specified user. Available only for admin.
```
-PUT /users/:uid/unblock
+PUT /users/:id/unblock
```
Parameters:
-- `uid` (required) - id of specified user
+- `id` (required) - id of specified user
Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 6b90940c047..545cc72682d 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,6 +1,6 @@
-## GitLab CI Documentation
+# GitLab CI Documentation
-### CI User documentation
+## CI User documentation
- [Get started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
@@ -20,4 +20,8 @@
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
-- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
+- [Review Apps](review_apps/index.md)
+
+## Breaking changes
+
+- [New CI build permissions model](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 959741f7338..89088cf9b83 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -44,7 +44,8 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
2. Install Docker Engine on server.
- For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/).
+ For more information how to install Docker Engine on different systems
+ checkout the [Supported installations](https://docs.docker.com/engine/installation/).
3. Add `gitlab-runner` user to `docker` group:
@@ -122,11 +123,17 @@ In order to do that, follow the steps:
Insecure = false
```
-1. You can now use `docker` in the build script (note the inclusion of the `docker:dind` service):
+1. You can now use `docker` in the build script (note the inclusion of the
+ `docker:dind` service):
```yaml
image: docker:latest
+ # When using dind, it's wise to use the overlayfs driver for
+ # improved performance.
+ variables:
+ DOCKER_DRIVER: overlay
+
services:
- docker:dind
@@ -140,15 +147,21 @@ In order to do that, follow the steps:
- docker run my-docker-image /script/to/run/tests
```
-Docker-in-Docker works well, and is the recommended configuration, but it is not without its own challenges:
-* By enabling `--docker-privileged`, you are effectively disabling all of
-the security mechanisms of containers and exposing your host to privilege
-escalation which can lead to container breakout. For more information, check out the official Docker documentation on
-[Runtime privilege and Linux capabilities][docker-cap].
-* Using docker-in-docker, each build is in a clean environment without the past
-history. Concurrent builds work fine because every build gets it's own instance of docker engine so they won't conflict with each other. But this also means builds can be slower because there's no caching of layers.
-* By default, `docker:dind` uses `--storage-driver vfs` which is the slowest form
-offered.
+Docker-in-Docker works well, and is the recommended configuration, but it is
+not without its own challenges:
+
+- By enabling `--docker-privileged`, you are effectively disabling all of
+ the security mechanisms of containers and exposing your host to privilege
+ escalation which can lead to container breakout. For more information, check
+ out the official Docker documentation on
+ [Runtime privilege and Linux capabilities][docker-cap].
+- Using docker-in-docker, each build is in a clean environment without the past
+ history. Concurrent builds work fine because every build gets it's own
+ instance of Docker engine so they won't conflict with each other. But this
+ also means builds can be slower because there's no caching of layers.
+- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
+ form offered. To use a different driver, see
+ [Using the overlayfs driver](#using-the-overlayfs-driver).
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
@@ -221,6 +234,40 @@ work as expected since volume mounting is done in the context of the host
machine, not the build container.
e.g. `docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests`
+## Using the OverlayFS driver
+
+By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
+copies the filesystem on every run. This is a very disk-intensive operation
+which can be avoided if a different driver is used, for example `overlay`.
+
+1. Make sure a recent kernel is used, preferably `>= 4.2`.
+1. Check whether the `overlay` module is loaded:
+
+ ```
+ sudo lsmod | grep overlay
+ ```
+
+ If you see no result, then it isn't loaded. To load it use:
+
+ ```
+ sudo modprobe overlay
+ ```
+
+ If everything went fine, you need to make sure module is loaded on reboot.
+ On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
+ following line into it:
+
+ ```
+ overlay
+ ```
+
+1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
+
+ ```
+ variables:
+ DOCKER_DRIVER: overlay
+ ```
+
## Using the GitLab Container Registry
> **Note:**
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index e070302fb82..9dd84a5ff81 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -3,69 +3,523 @@
>**Note:**
Introduced in GitLab 8.9.
-## Environments
+During the development of software, there can be many stages until it's ready
+for public consumption. You sure want to first test your code and then deploy it
+in a testing or staging environment before you release it to the public. That
+way you can prevent bugs not only in your software, but in the deployment
+process as well.
-Environments are places where code gets deployed, such as staging or production.
-CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment.
-Defining environments in a project's `.gitlab-ci.yml` lets developers track
-[deployments] to these environments.
+GitLab CI is capable of not only testing or building your projects, but also
+deploying them in your infrastructure, with the added benefit of giving you a
+way to track your deployments. In other words, you can always know what is
+currently being deployed or has been deployed on your servers.
-## Deployments
+## Overview
-Deployments are created when [jobs] deploy versions of code to [environments].
+With environments, you can control the Continuous Deployment of your software
+all within GitLab. All you need to do is define them in your project's
+[`.gitlab-ci.yml`][yaml] as we will explore below. GitLab provides a full
+history of your deployments per every environment.
-### Checkout deployments locally
+Environments are like tags for your CI jobs, describing where code gets deployed.
+Deployments are created when [jobs] deploy versions of code to environments,
+so every environment can have one or more deployments. GitLab keeps track of
+your deployments, so you always know what is currently being deployed on your
+servers.
-Since 8.13, a reference in the git repository is saved for each deployment. So
-knowing what the state is of your current environments is only a `git fetch`
-away.
+To better understand how environments and deployments work, let's consider an
+example. We assume that you have already created a project in GitLab and set up
+a Runner. The example will cover the following:
-In your git config, append the `[remote "<your-remote>"]` block with an extra
-fetch line:
+- We are developing an application
+- We want to run tests and build our app on all branches
+- Our default branch is `master`
+- We deploy the app only when a pipeline on `master` branch is run
+
+Let's see how it all ties together.
+
+## Defining environments
+Let's consider the following `.gitlab-ci.yml` example:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
```
-fetch = +refs/environments/*:refs/remotes/origin/environments/*
+
+We have defined 3 [stages](yaml/README.md#stages):
+
+- test
+- build
+- deploy
+
+The jobs assigned to these stages will run in this order. If a job fails, then
+the builds that are assigned to the next stage won't run, rendering the pipeline
+as failed. In our case, the `test` job will run first, then the `build` and
+lastly the `deploy_staging`. With this, we ensure that first the tests pass,
+then our app is able to be built successfully, and lastly we deploy to the
+staging server.
+
+The `environment` keyword is just a hint for GitLab that this job actually
+deploys to this environment's `name`. It can also have a `url` which, as we
+will later see, is exposed in various places within GitLab. Each time a job that
+has an environment specified and succeeds, a deployment is recorded, remembering
+the Git SHA and environment name.
+
+To sum up, with the above `.gitlab-ci.yml` we have achieved that:
+
+- All branches will run the `test` and `build` jobs.
+- The `deploy_staging` job will run [only](yaml/README.md#only) on the `master`
+ branch which means all merge requests that are created from branches don't
+ get to deploy to the staging server
+- When a merge request is merged, all jobs will run and the `deploy_staging`
+ in particular will deploy our code to a staging server while the deployment
+ will be recorded in an environment named `staging`.
+
+Let's now see how that information is exposed within GitLab.
+
+## Viewing the current status of an environment
+
+The environment list under your project's **Pipelines ➔ Environments**, is
+where you can find information of the last deployment status of an environment.
+
+Here's how the Environments page looks so far.
+
+![Staging environment view](img/environments_available_staging.png)
+
+There's a bunch of information there, specifically you can see:
+
+- The environment's name with a link to its deployments
+- The last deployment ID number and who performed it
+- The build ID of the last deployment with its respective job name
+- The commit information of the last deployment such as who committed, to what
+ branch and the Git SHA of the commit
+- The exact time the last deployment was performed
+- A button that takes you to the URL that you have defined under the
+ `environment` keyword in `.gitlab-ci.yml`
+- A button that re-deploys the latest deployment, meaning it runs the job
+ defined by the environment name for that specific commit
+
+>**Notes:**
+- While you can create environments manually in the web interface, we recommend
+ that you define your environments in `.gitlab-ci.yml` first. They will
+ be automatically created for you after the first deploy.
+- The environments page can only be viewed by Reporters and above. For more
+ information on the permissions, see the [permissions documentation][permissions].
+- Only deploys that happen after your `.gitlab-ci.yml` is properly configured
+ will show up in the "Environment" and "Last deployment" lists.
+
+The information shown in the Environments page is limited to the latest
+deployments, but as you may have guessed an environment can have multiple
+deployments.
+
+## Viewing the deployment history of an environment
+
+GitLab keeps track of your deployments, so you always know what is currently
+being deployed on your servers. That way you can have the full history of your
+deployments per every environment right in your browser. Clicking on an
+environment will show the history of its deployments. Assuming you have deployed
+multiple times already, here's how a specific environment's page looks like.
+
+![Deployments](img/deployments_view.png)
+
+We can see the same information as when in the Environments page, but this time
+all deployments are shown. As you may have noticed, apart from the **Re-deploy**
+button there are now **Rollback** buttons for each deployment. Let's see how
+that works.
+
+## Rolling back changes
+
+You can't control everything, so sometimes things go wrong. When that unfortunate
+time comes GitLab has you covered. Simply by clicking the **Rollback** button
+that can be found in the deployments page
+(**Pipelines ➔ Environments ➔ `environment name`**) you can relaunch the
+job with the commit associated with it.
+
+>**Note:**
+Bare in mind that your mileage will vary and it's entirely up to how you define
+the deployment process in the job's `script` whether the rollback succeeds or not.
+GitLab CI is just following orders.
+
+Thankfully that was the staging server that we had to rollback, and since we
+learn from our mistakes, we decided to not make the same again when we deploy
+to the production server. Enter manual actions for deployments.
+
+## Manually deploying to environments
+
+Turning a job from running automatically to a manual action is as simple as
+adding `when: manual` to it. To expand on our previous example, let's add
+another job that this time deploys our app to a production server and is
+tracked by a `production` environment. The `.gitlab-ci.yml` looks like this
+so far:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
```
-## Defining environments
+The `when: manual` action exposes a play button in GitLab's UI and the
+`deploy_prod` job will only be triggered if and when we click that play button.
+You can find it in the pipeline, build, environment, and deployment views.
+
+| Pipelines | Single pipeline | Environments | Deployments | Builds |
+| --------- | ----------------| ------------ | ----------- | -------|
+| ![Pipelines manual action](img/environments_manual_action_pipelines.png) | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) | ![Environments manual action](img/environments_manual_action_environments.png) | ![Deployments manual action](img/environments_manual_action_deployments.png) | ![Builds manual action](img/environments_manual_action_builds.png) |
+
+Clicking on the play button in either of these places will trigger the
+`deploy_prod` job, and the deployment will be recorded under a new
+environment named `production`.
+
+>**Note:**
+Remember that if your environment's name is `production` (all lowercase), then
+it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
+Double the benefit!
+
+While this is fine for deploying to some stable environments like staging or
+production, what happens for branches? So far we haven't defined anything
+regarding deployments for branches other than `master`. Dynamic environments
+will help us achieve that.
+
+## Dynamic environments
-You can create and delete environments manually in the web interface, but we
-recommend that you define your environments in `.gitlab-ci.yml` first, which
-will automatically create environments for you after the first deploy.
+As the name suggests, it is possible to create environments on the fly by just
+declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
+the basis of [Review apps](review_apps/index.md).
-The `environment` is just a hint for GitLab that this job actually deploys to
-this environment. Each time the job succeeds, a deployment is recorded,
-remembering the git SHA and environment.
+GitLab Runner exposes various [environment variables][variables] when a job runs,
+and as such, you can use them as environment names. Let's add another job in
+our example which will deploy to all branches except `master`:
-Add something like this to your `.gitlab-ci.yml`:
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.example.com
+ only:
+ - branches
+ except:
+ - master
```
-production:
+
+Let's break it down in pieces. The job's name is `deploy_review` and it runs
+on the `deploy` stage. The `script` at this point is fictional, you'd have to
+use your own based on your deployment. Then, we set the `environment` with the
+`environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
+one. Since the [environment name][env-name] can contain also slashes (`/`), we
+can use this pattern to distinguish between dynamic environments and the regular
+ones.
+
+So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
+which takes the value of the branch name. We also use the same
+`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment
+can get a specific and distinct URL for each branch. Again, the way you set up
+the webserver to serve these requests is based on your setup.
+
+Last but not least, we tell the job to run [`only`][only] on branches
+[`except`][only] master.
+
+>**Note:**
+You are not bound to use the same prefix or only slashes in the dynamic
+environments' names (`/`), but as we will see later, this will enable the
+[grouping similar environments](#grouping-similar-environments) feature.
+
+The whole `.gitlab-ci.yml` looks like this so far:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.example.com
+ only:
+ - branches
+ except:
+ - master
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
stage: deploy
- script: dpl...
- environment: production
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
```
-See full [documentation](yaml/README.md#environment).
+A more realistic example would include copying files to a location where a
+webserver (NGINX) could then read and serve. The example below will copy the
+`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`:
-## Seeing environment status
+```yaml
+review_app:
+ stage: deploy
+ script:
+ - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.example.com
+```
-You can find the environment list under **Pipelines > Environments** for your
-project. You'll see the git SHA and date of the last deployment to each
-environment defined.
+It is assumed that the user has already setup NGINX and GitLab Runner in the
+server this job will run on.
>**Note:**
-Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
-show up in the environments and deployments lists.
+Be sure to check out the [limitations](#limitations) section for some edge
+cases regarding naming of you branches and Review Apps.
+
+---
+
+The development workflow would now be:
+
+- Developer creates a branch locally
+- Developer makes changes, commits and pushes the branch to GitLab
+- Developer creates a merge request
+
+Behind the scenes:
+
+- GitLab Runner picks up the changes and starts running the jobs
+- The jobs run sequentially as defined in `stages`
+ - First, the tests pass
+ - Then, the build begins and successfully also passes
+ - Lastly, the app is deployed to an environment with a name specific to the
+ branch
+
+So now, every branch gets its own environment and is deployed to its own place
+with the added benefit of having a [history of deployments](#viewing-the-deployment-history-of-an-environment)
+and also being able to [rollback changes](#rolling-back-changes) if needed.
+Let's briefly see where URL that's defined in the environments is exposed.
+
+## Making use of the environment URL
+
+The environment URL is exposed in a few places within GitLab.
+
+| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
+| -------------------- | ------------ | ----------- |
+| ![Environment URL in merge request](img/environments_mr_review_app.png) | ![Environment URL in environments](img/environments_link_url.png) | ![Environment URL in deployments](img/environments_link_url_deployments.png) |
+
+If a merge request is eventually merged to the default branch (in our case
+`master`) and that branch also deploys to an environment (in our case `staging`
+and/or `production`) you can see this information in the merge request itself.
+
+![Environment URLs in merge request](img/environments_link_url_mr.png)
+
+---
+
+We now have a full development cycle, where our app is tested, built, deployed
+as a Review app, deployed to a staging server once the merge request is merged,
+and finally manually deployed to the production server. What we just described
+is a single workflow, but imagine tens of developers working on a project
+at the same time. They each push to their branches, and dynamic environments are
+created all the time. In that case, we probably need to do some clean up. Read
+next how environments can be stopped.
+
+## Stopping an environment
+
+By stopping an environment, you are effectively terminating its recording of the
+deployments that happen in it.
+
+A branch is associated with an environment when the CI pipeline that is created
+for this branch, was recently deployed to this environment. You can think of
+the CI pipeline as the glue between the branch and the environment:
+`branch ➔ CI pipeline ➔ environment`.
+
+There is a special case where environments can be manually stopped. That can
+happen if you provide another job for that matter. The syntax is a little
+tricky since a job calls another job to do the job.
+
+Consider the following example where the `deploy_review` calls the `stop_review`
+to clean up and stop the environment:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ url: https://$CI_BUILD_REF_NAME.example.com
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
-## Seeing deployment history
+stop_review:
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - echo "Remove review app"
+ when: manual
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+ action: stop
+```
-Clicking on an environment will show the history of deployments.
+Setting the [`GIT_STRATEGY`][git-strategy] to `none` is necessary on the
+`stop_review` job so that the [GitLab Runner] won't try to checkout the code
+after the branch is deleted.
>**Note:**
-Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
-show up in the environments and deployments lists.
+Starting with GitLab 8.14, dynamic environments will be stopped automatically
+when their associated branch is deleted.
+
+When you have an environment that has a stop action defined (typically when
+the environment describes a review app), GitLab will automatically trigger a
+stop action when the associated branch is deleted.
+
+You can read more in the [`.gitlab-ci.yml` reference][onstop].
+
+## Grouping similar environments
+
+> [Introduced][ce-7015] in GitLab 8.14.
+
+As we've seen in the [dynamic environments](#dynamic-environments), you can
+prepend their name with a word, then followed by a `/` and finally the branch
+name which is automatically defined by the `CI_BUILD_REF_NAME` variable.
+
+In short, environments that are named like `type/foo` are presented under a
+group named `type`.
+
+In our minimal example, we name the environments `review/$CI_BUILD_REF_NAME`
+where `$CI_BUILD_REF_NAME` is the branch name:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_BUILD_REF_NAME
+```
+
+In that case, if you visit the Environments page, and provided the branches
+exist, you should see something like:
+
+![Environment groups](img/environments_dynamic_groups.png)
+
+## Checkout deployments locally
+
+Since 8.13, a reference in the git repository is saved for each deployment. So
+knowing what the state is of your current environments is only a `git fetch`
+away.
+
+In your git config, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
+## Limitations
+
+1. If the branch name contains special characters (`/`), and you use the
+ `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
+ be complications during your Review Apps deployment. Follow the
+ [issue 22849][ce-22849] for more information.
+1. You are limited to use only the [CI predefined variables][variables] in the
+ `environment: name`. If you try to re-use variables defined inside `script`
+ as part of the environment name, it will not work.
+
+## Further reading
+
+Below are some links you may find interesting:
+
+- [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
+- [A blog post on Deployments & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
+[yaml]: yaml/README.md
[environments]: #environments
[deployments]: #deployments
+[permissions]: ../user/permissions.md
+[variables]: variables/README.md
+[env-name]: yaml/README.md#environment-name
+[only]: yaml/README.md#only-and-except
+[onstop]: yaml/README.md#environment-on_stop
+[ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015
+[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849
+[gitlab runner]: https://docs.gitlab.com/runner/
+[git-strategy]: yaml/README.md#git-strategy
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 7412fdbbc78..85f8849fa99 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -1,11 +1,11 @@
-## Test a Scala application
+# Test and deploy to Heroku a Scala application
This example demonstrates the integration of Gitlab CI with Scala
applications using SBT. Checkout the example
[project](https://gitlab.com/gitlab-examples/scala-sbt) and
[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
-### Add `.gitlab-ci.yml` file to project
+## Add `.gitlab-ci.yml` file to project
The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
@@ -13,10 +13,14 @@ repository to trigger CI:
``` yaml
image: java:8
+stages:
+ - test
+ - deploy
+
before_script:
- apt-get update -y
- apt-get install apt-transport-https -y
- # Install SBT
+ ## Install SBT
- echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
- apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
- apt-get update -y
@@ -24,8 +28,17 @@ before_script:
- sbt sbt-version
test:
+ stage: test
script:
- sbt clean coverage test coverageReport
+
+deploy:
+ stage: deploy
+ script:
+ - apt-get update -yq
+ - apt-get install rubygems ruby-dev -y
+ - gem install dpl
+ - dpl --provider=heroku --app=gitlab-play-sample-app --api-key=$HEROKU_API_KEY
```
The `before_script` installs [SBT](http://www.scala-sbt.org/) and
@@ -33,15 +46,31 @@ displays the version that is being used. The `test` stage executes SBT
to compile and test the project.
[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
plugin to measure test coverage.
+The `deploy` stage automatically deploys the project to Heroku using dpl.
You can use other versions of Scala and SBT by defining them in
`build.sbt`.
-### Display test coverage in build
+## Display test coverage in build
Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
-**Settings > Edit Project > Test coverage parsing** project setting to
-retrieve the test coverage rate from the build trace and have it
+**Settings ➔ Edit Project ➔ Test coverage parsing** project setting to
+retrieve the [test coverage] rate from the build trace and have it
displayed with your builds.
**Builds** must be enabled for this option to appear.
+
+## Heroku application
+
+A Heroku application is required. You can create one through the
+[Dashboard](https://dashboard.heroku.com/). Substitute `gitlab-play-sample-app`
+in the `.gitlab-ci.yml` file with your application's name.
+
+## Heroku API key
+
+You can look up your Heroku API key in your
+[account](https://dashboard.heroku.com/account). Add a secure [variable] with
+this value in **Project ➔ Variables** with key `HEROKU_API_KEY`.
+
+[variable]: ../variables/README.md#user-defined-variables-secure-variables
+[test coverage]: ../../user/project/pipelines/settings.md#test-coverage-report-badge
diff --git a/doc/ci/img/deployments_view.png b/doc/ci/img/deployments_view.png
new file mode 100644
index 00000000000..ca6097cbea4
--- /dev/null
+++ b/doc/ci/img/deployments_view.png
Binary files differ
diff --git a/doc/ci/img/environments_available_staging.png b/doc/ci/img/environments_available_staging.png
new file mode 100644
index 00000000000..784c4fd944c
--- /dev/null
+++ b/doc/ci/img/environments_available_staging.png
Binary files differ
diff --git a/doc/ci/img/environments_dynamic_groups.png b/doc/ci/img/environments_dynamic_groups.png
new file mode 100644
index 00000000000..e89b66c502c
--- /dev/null
+++ b/doc/ci/img/environments_dynamic_groups.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url.png b/doc/ci/img/environments_link_url.png
new file mode 100644
index 00000000000..224c21adfb5
--- /dev/null
+++ b/doc/ci/img/environments_link_url.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url_deployments.png b/doc/ci/img/environments_link_url_deployments.png
new file mode 100644
index 00000000000..9419668a9bd
--- /dev/null
+++ b/doc/ci/img/environments_link_url_deployments.png
Binary files differ
diff --git a/doc/ci/img/environments_link_url_mr.png b/doc/ci/img/environments_link_url_mr.png
new file mode 100644
index 00000000000..3276dfb6096
--- /dev/null
+++ b/doc/ci/img/environments_link_url_mr.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_builds.png b/doc/ci/img/environments_manual_action_builds.png
new file mode 100644
index 00000000000..d4bb7ccdbae
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_builds.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_deployments.png b/doc/ci/img/environments_manual_action_deployments.png
new file mode 100644
index 00000000000..c2477381c80
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_deployments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_environments.png b/doc/ci/img/environments_manual_action_environments.png
new file mode 100644
index 00000000000..56601c0db2d
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_environments.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_pipelines.png b/doc/ci/img/environments_manual_action_pipelines.png
new file mode 100644
index 00000000000..eb6e87cd956
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_pipelines.png
Binary files differ
diff --git a/doc/ci/img/environments_manual_action_single_pipeline.png b/doc/ci/img/environments_manual_action_single_pipeline.png
new file mode 100644
index 00000000000..9713ad212e2
--- /dev/null
+++ b/doc/ci/img/environments_manual_action_single_pipeline.png
Binary files differ
diff --git a/doc/ci/img/environments_mr_review_app.png b/doc/ci/img/environments_mr_review_app.png
new file mode 100644
index 00000000000..a2ae25d62fa
--- /dev/null
+++ b/doc/ci/img/environments_mr_review_app.png
Binary files differ
diff --git a/doc/ci/img/environments_view.png b/doc/ci/img/environments_view.png
new file mode 100644
index 00000000000..131a9718cc4
--- /dev/null
+++ b/doc/ci/img/environments_view.png
Binary files differ
diff --git a/doc/ci/review_apps/img/review_apps_preview_in_mr.png b/doc/ci/review_apps/img/review_apps_preview_in_mr.png
new file mode 100644
index 00000000000..15bcb90518c
--- /dev/null
+++ b/doc/ci/review_apps/img/review_apps_preview_in_mr.png
Binary files differ
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
new file mode 100644
index 00000000000..a66165dc973
--- /dev/null
+++ b/doc/ci/review_apps/index.md
@@ -0,0 +1,124 @@
+# Getting started with Review Apps
+
+>
+- [Introduced][ce-21971] in GitLab 8.12. Further additions were made in GitLab
+ 8.13 and 8.14.
+- Inspired by [Heroku's Review Apps][heroku-apps] which itself was inspired by
+ [Fourchette].
+
+The basis of Review Apps is the [dynamic environments] which allow you to create
+a new environment (dynamically) for each one of your branches.
+
+A Review App can then be visible as a link when you visit the [merge request]
+relevant to the branch. That way, you are able to see live all changes introduced
+by the merge request changes. Reviewing anything, from performance to interface
+changes, becomes much easier with a live environment and as such, Review Apps
+can make a huge impact on your development flow.
+
+They mostly make sense to be used with web applications, but you can use them
+any way you'd like.
+
+## Overview
+
+Simply put, a Review App is a mapping of a branch with an environment as there
+is a 1:1 relation between them.
+
+Here's an example of what it looks like when viewing a merge request with a
+dynamically set environment.
+
+![Review App in merge request](img/review_apps_preview_in_mr.png)
+
+In the image above you can see that the `add-new-line` branch was successfully
+built and deployed under a dynamic environment and can be previewed with an
+also dynamically URL.
+
+The details of the Review Apps implementation depend widely on your real
+technology stack and on your deployment process. The simplest case it to
+deploy a simple static HTML website, but it will not be that straightforward
+when your app is using a database for example. To make a branch be deployed
+on a temporary instance and booting up this instance with all required software
+and services automatically on the fly is not a trivial task. However, it is
+doable, especially if you use Docker, or at least a configuration management
+tool like Chef, Puppet, Ansible or Salt.
+
+## Prerequisites
+
+To get a better understanding of Review Apps, you must first learn how
+environments and deployments work. The following docs will help you grasp that
+knowledge:
+
+1. First, learn about [environments][] and their role in the development workflow.
+1. Then make a small stop to learn about [CI variables][variables] and how they
+ can be used in your CI jobs.
+1. Next, explore the [`environment` syntax][yaml-env] as defined in `.gitlab-ci.yml`.
+ This will be your primary reference when you are finally comfortable with
+ how environments work.
+1. Additionally, find out about [manual actions][] and how you can use them to
+ deploy to critical environments like production with the push of a button.
+1. And as a last step, follow the [example tutorials](#examples) which will
+ guide you step by step to set up the infrastructure and make use of
+ Review Apps.
+
+## Configuration
+
+The configuration of Review apps depends on your technology stack and your
+infrastructure. Read the [dynamic environments] documentation to understand
+how to define and create them.
+
+## Creating and destroying Review Apps
+
+The creation and destruction of a Review App is defined in `.gitlab-ci.yml`
+at a job level under the `environment` keyword.
+
+Check the [environments] documentation how to do so.
+
+## A simple workflow
+
+The process of adding Review Apps in your workflow would look like:
+
+1. Set up the infrastructure to host and deploy the Review Apps.
+1. [Install][install-runner] and [configure][conf-runner] a Runner that does
+ the deployment.
+1. Set up a job in `.gitlab-ci.yml` that uses the predefined
+ [predefined CI environment variable][variables] `${CI_BUILD_REF_NAME}` to
+ create dynamic environments and restrict it to run only on branches.
+1. Optionally set a job that [manually stops][manual-env] the Review Apps.
+
+From there on, you would follow the branched Git flow:
+
+1. Push a branch and let the Runner deploy the Review App based on the `script`
+ definition of the dynamic environment job.
+1. Wait for the Runner to build and/or deploy your web app.
+1. Click on the link that's present in the MR related to the branch and see the
+ changes live.
+
+## Limitations
+
+Check the [environments limitations](../environments.md#limitations).
+
+## Examples
+
+A list of examples used with Review Apps can be found below:
+
+- [Use with NGINX][app-nginx] - Use NGINX and the shell executor of GitLab Runner
+ to deploy a simple HTML website.
+
+And below is a soon to be added examples list:
+
+- Use with Amazon S3
+- Use on Heroku with dpl
+- Use with OpenShift/kubernetes
+
+[app-nginx]: https://gitlab.com/gitlab-examples/review-apps-nginx
+[ce-21971]: https://gitlab.com/gitlab-org/gitlab-ce/issues/21971
+[dynamic environments]: ../environments.md#dynamic-environments
+[environments]: ../environments.md
+[fourchette]: https://github.com/rainforestapp/fourchette
+[heroku-apps]: https://devcenter.heroku.com/articles/github-integration-review-apps
+[manual actions]: ../environments.md#manual-actions
+[merge request]: ../../user/project/merge_requests.md
+[variables]: ../variables/README.md
+[yaml-env]: ../yaml/README.md#environment
+[install-runner]: https://docs.gitlab.com/runner/install/
+[conf-runner]: https://docs.gitlab.com/runner/commands/
+[manual-env]: ../environments.md#stopping-an-environment
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 84048f1d25f..cf7c55f75f2 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -58,6 +58,22 @@ below.
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
+## Trigger a build from webhook
+
+> Introduced in GitLab 8.14.
+
+To trigger a build from webhook of another project you need to add the following
+webhook url for Push and Tag push events:
+
+```
+https://gitlab.example.com/api/v3/projects/:id/ref/:ref/trigger/builds?token=TOKEN
+```
+
+> **Note**:
+- `ref` should be passed as part of url in order to take precedence over `ref`
+ from webhook body that designates the branchref that fired the trigger in the source repository.
+- `ref` should be url encoded if contains slashes.
+
## Pass build variables to a trigger
You can pass any number of arbitrary variables in the trigger API call and they
@@ -169,6 +185,14 @@ curl --request POST \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
+### Using webhook to trigger builds
+
+You can add the following webhook to another project in order to trigger a build:
+
+```
+https://gitlab.example.com/api/v3/projects/9/ref/master/trigger/builds?token=TOKEN&variables[UPLOAD_TO_S3]=true
+```
+
### Using cron to trigger nightly builds
Whether you craft a script or just run cURL directly, you can trigger builds
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5c0e1c44e3f..338c9a27789 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -541,6 +541,8 @@ same manual action multiple times.
An example usage of manual actions is deployment to production.
+Read more at the [environments documentation][env-manual].
+
### environment
> Introduced in GitLab 8.9.
@@ -552,28 +554,14 @@ An example usage of manual actions is deployment to production.
If `environment` is specified and no environment under that name exists, a new
one will be created automatically.
-The `environment` name can contain:
-
-- letters
-- digits
-- spaces
-- `-`
-- `_`
-- `/`
-- `$`
-- `{`
-- `}`
-
-Common names are `qa`, `staging`, and `production`, but you can use whatever
-name works with your workflow.
-
In its simplest form, the `environment` keyword can be defined like:
```
deploy to production:
stage: deploy
script: git push production HEAD:master
- environment: production
+ environment:
+ name: production
```
In the above example, the `deploy to production` job will be marked as doing a
@@ -588,6 +576,21 @@ Before GitLab 8.11, the name of an environment could be defined as a string like
`environment: production`. The recommended way now is to define it under the
`name` keyword.
+The `environment` name can contain:
+
+- letters
+- digits
+- spaces
+- `-`
+- `_`
+- `/`
+- `$`
+- `{`
+- `}`
+
+Common names are `qa`, `staging`, and `production`, but you can use whatever
+name works with your workflow.
+
Instead of defining the name of the environment right after the `environment`
keyword, it is also possible to define it as a separate value. For that, use
the `name` keyword under `environment`:
@@ -626,7 +629,12 @@ deploy to production:
#### environment:on_stop
-> [Introduced][ce-6669] in GitLab 8.13.
+>
+**Notes:**
+- [Introduced][ce-6669] in GitLab 8.13.
+- Starting with GitLab 8.14, when you have an environment that has a stop action
+ defined, GitLab will automatically trigger a stop action when the associated
+ branch is deleted.
Closing (stoping) environments can be achieved with the `on_stop` keyword defined under
`environment`. It declares a different job that runs in order to close
@@ -681,6 +689,13 @@ The `stop_review_app` job is **required** to have the following keywords defined
These parameters can use any of the defined [CI variables](#variables)
(including predefined, secure variables and `.gitlab-ci.yml` variables).
+>**Note:**
+Be aware than if the branch name contains special characters and you use the
+`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
+be complications during deployment. Follow the
+[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more
+information.
+
For example:
```
@@ -745,6 +760,15 @@ artifacts:
- binaries/
```
+To disable artifact passing, define the job with empty [dependencies](#dependencies):
+
+```yaml
+job:
+ stage: build
+ script: make build
+ dependencies: []
+```
+
You may want to create artifacts only for tagged releases to avoid filling the
build server storage with temporary build artifacts.
@@ -1210,6 +1234,7 @@ capitalization, the commit will be created but the builds will be skipped.
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
+[env-manual]: ../environments.md#manually-deploying-to-environments
[examples]: ../examples/README.md
[ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323
[environment]: ../environments.md
diff --git a/doc/development/README.md b/doc/development/README.md
index bf1f054b7d5..371bb55c127 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -14,7 +14,7 @@
contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
-- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
+- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
@@ -22,6 +22,7 @@
## Process
- [Generate a changelog entry with `bin/changelog`](changelog.md)
+- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index ec8f2d6531c..9e782ab977f 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -205,6 +205,57 @@ command line.
Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake teaspoon:fixtures`.
+## Design Patterns
+
+### Singletons
+
+When exactly one object is needed for a given task, prefer to define it as a
+`class` rather than as an object literal. Prefer also to explicitly restrict
+instantiation, unless flexibility is important (e.g. for testing).
+
+```
+// bad
+
+gl.MyThing = {
+ prop1: 'hello',
+ method1: () => {}
+};
+
+// good
+
+class MyThing {
+ constructor() {
+ this.prop1 = 'hello';
+ }
+ method1() {}
+}
+
+gl.MyThing = new MyThing();
+
+// best
+
+let singleton;
+
+class MyThing {
+ constructor() {
+ if (!singleton) {
+ singleton = this;
+ singleton.init();
+ }
+ return singleton;
+ }
+
+ init() {
+ this.prop1 = 'hello';
+ }
+
+ method1() {}
+}
+
+gl.MyThing = MyThing;
+
+```
+
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index b25ce79e89f..7bfc9cb361f 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -32,6 +32,95 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth
Except for the top-level `describe` block, always provide a String argument to
`describe`.
+## Don't assert against the absolute value of a sequence-generated attribute
+
+Consider the following factory:
+
+```ruby
+FactoryGirl.define do
+ factory :label do
+ sequence(:title) { |n| "label#{n}" }
+ end
+end
+```
+
+Consider the following API spec:
+
+```ruby
+require 'rails_helper'
+
+describe API::Labels do
+ it 'creates a first label' do
+ create(:label)
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('label1')
+ end
+
+ it 'creates a second label' do
+ create(:label)
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('label1')
+ end
+end
+```
+
+When run, this spec doesn't do what we might expect:
+
+```sh
+1) API::API reproduce sequence issue creates a second label
+ Failure/Error: expect(json_response.first['name']).to eq('label1')
+
+ expected: "label1"
+ got: "label2"
+
+ (compared using ==)
+```
+
+That's because FactoryGirl sequences are not reseted for each example.
+
+Please remember that sequence-generated values exist only to avoid having to
+explicitly set attributes that have a uniqueness constraint when using a factory.
+
+### Solution
+
+If you assert against a sequence-generated attribute's value, you should set it
+explicitly. Also, the value you set shouldn't match the sequence pattern.
+
+For instance, using our `:label` factory, writing `create(:label, title: 'foo')`
+is ok, but `create(:label, title: 'label1')` is not.
+
+Following is the fixed API spec:
+
+```ruby
+require 'rails_helper'
+
+describe API::Labels do
+ it 'creates a first label' do
+ create(:label, title: 'foo')
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('foo')
+ end
+
+ it 'creates a second label' do
+ create(:label, title: 'bar')
+
+ get api("/projects/#{project.id}/labels", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['name']).to eq('bar')
+ end
+end
+```
+
## Don't `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index 105e2f1242a..b8669964c84 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -24,7 +24,7 @@ namespace you can use the `configure` class method. This method simply yields
the supplied block while passing `Gitlab::Metrics::Instrumentation` as its
argument. An example:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(Foo, :bar)
conf.instrument_method(Foo, :baz)
@@ -41,7 +41,7 @@ Method instrumentation should be added in the initializer
Instrumenting a single method:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_method(User, :find_by)
end
@@ -49,7 +49,7 @@ end
Instrumenting an entire class hierarchy:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_class_hierarchy(ActiveRecord::Base)
end
@@ -57,7 +57,7 @@ end
Instrumenting all public class methods:
-```
+```ruby
Gitlab::Metrics::Instrumentation.configure do |conf|
conf.instrument_methods(User)
end
@@ -68,7 +68,7 @@ end
The easiest way to check if a method has been instrumented is to check its
source location. For example:
-```
+```ruby
method = Rugged::TagCollection.instance_method(:[])
method.source_location
diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md
new file mode 100644
index 00000000000..b7e6387838e
--- /dev/null
+++ b/doc/development/limit_ee_conflicts.md
@@ -0,0 +1,272 @@
+# Limit conflicts with EE when developing on CE
+
+This guide contains best-practices for avoiding conflicts between CE and EE.
+
+## Context
+
+Usually, GitLab Community Edition is merged into the Enterprise Edition once a
+week. During these merges, it's very common to get conflicts when some changes
+in CE do not apply cleanly to EE.
+
+There are a few things that can help you as a developer to:
+
+- know when your merge request to CE will conflict when merged to EE
+- avoid such conflicts in the first place
+- ease future conflict resolutions if conflict is inevitable
+
+## Check the `rake ee_compat_check` in your merge requests
+
+For each commit (except on `master`), the `rake ee_compat_check` CI job tries to
+detect if the current branch's changes will conflict during the CE->EE merge.
+
+The job reports what files are conflicting and how to setup a merge request
+against EE. Here is roughly how it works:
+
+1. Generates the diff between your branch and current CE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds, otherwise...
+1. Detects a branch with the `-ee` suffix in EE
+1. If it exists, generate the diff between this branch and current EE `master`
+1. Tries to apply it to current EE `master`
+1. If it applies cleanly, the job succeeds
+
+In the case where the job fails, it means you should create a `<ce_branch>-ee`
+branch, push it to EE and open a merge request against EE `master`. At this
+point if you retry the failing job in your CE merge request, it should now pass.
+
+Notes:
+
+- This task is not a silver-bullet, its current goal is to bring awareness to
+ developers that their work needs to be ported to EE.
+- Community contributors shouldn't submit merge requests against EE, but
+ reviewers should take actions by either creating such EE merge request or
+ asking a GitLab developer to do it once the merge request is merged.
+- If you branch is more than 500 commits behind `master`, the job will fail and
+ you should rebase your branch upon latest `master`.
+
+## Possible type of conflicts
+
+### Controllers
+
+#### List or arrays are augmented in EE
+
+In controllers, the most common type of conflict is with `before_action` that
+has a list of actions in CE but EE adds some actions to that list.
+
+The same problem often occurs for `params.require` / `params.permit` calls.
+
+##### Mitigations
+
+Separate CE and EE actions/keywords. For instance for `params.require` in
+`ProjectsController`:
+
+```ruby
+def project_params
+ params.require(:project).permit(project_params_ce)
+ # On EE, this is always:
+ # params.require(:project).permit(project_params_ce << project_params_ee)
+end
+
+# Always returns an array of symbols, created however best fits the use case.
+# It _should_ be sorted alphabetically.
+def project_params_ce
+ %i[
+ description
+ name
+ path
+ ]
+end
+
+# (On EE)
+def project_params_ee
+ %i[
+ approvals_before_merge
+ approver_group_ids
+ approver_ids
+ ...
+ ]
+end
+```
+
+#### Additional condition(s) in EE
+
+For instance for LDAP:
+
+```diff
+ def destroy
+ @key = current_user.keys.find(params[:id])
+ - @key.destroy
+ + @key.destroy unless @key.is_a? LDAPKey
+
+ respond_to do |format|
+```
+
+Or for Geo:
+
+```diff
+def after_sign_out_path_for(resource)
+- current_application_settings.after_sign_out_path.presence || new_user_session_path
++ if Gitlab::Geo.secondary?
++ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
++ else
++ current_application_settings.after_sign_out_path.presence || new_user_session_path
++ end
+end
+```
+
+Or even for audit log:
+
+```diff
+def approve_access_request
+- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
++ member = Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
++
++ log_audit_event(member, action: :create)
+
+ redirect_to polymorphic_url([membershipable, :members])
+end
+```
+
+### Views
+
+#### Additional view code in EE
+
+A block of code added in CE conflicts because there is already another block
+at the same place in EE
+
+##### Mitigations
+
+Blocks of code that are EE-specific should be moved to partials as much as
+possible to avoid conflicts with big chunks of HAML code that that are not fun
+to resolve when you add the indentation to the equation.
+
+For instance this kind of thing:
+
+```haml
+- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ - has_due_date = issuable.has_attribute?(:due_date)
+ %hr
+ .row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = f.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = f.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label"
+
+ - if issuable.respond_to?(:weight)
+ .form-group
+ = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
+ Weight
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true },
+ { class: 'select2 js-select2', data: { placeholder: "Select weight" }}
+
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = f.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+```
+
+could be simplified by using partials:
+
+```haml
+= render 'metadata_form', issuable: issuable
+```
+
+and then the `_metadata_form.html.haml` could be as follows:
+
+```haml
+- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+%hr
+.row
+ %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") }
+ .form-group.issue-assignee
+ = f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ - if issuable.assignee_id
+ = f.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} })
+ .form-group.issue-milestone
+ = f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ .form-group
+ - has_labels = @labels && @labels.any?
+ = f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = f.hidden_field :label_ids, multiple: true, value: ''
+ .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }, dropdown_title: "Select label"
+
+ = render 'weight_form', issuable: issuable, has_due_date: has_due_date
+
+ - if has_due_date
+ .col-lg-6
+ .form-group
+ = f.label :due_date, "Due date", class: "control-label"
+ .col-sm-10
+ .issuable-form-select-holder
+ = f.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date"
+```
+
+and then the `_weight_form.html.haml` could be as follows:
+
+```haml
+- return unless issuable.respond_to?(:weight)
+
+- has_due_date = issuable.has_attribute?(:due_date)
+
+.form-group
+ = f.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do
+ Weight
+ .col-sm-10{ class: ("col-lg-8" if has_due_date) }
+ = f.select :weight, issues_weight_options(issuable.weight, edit: true), { include_blank: true },
+ { class: 'select2 js-select2', data: { placeholder: "Select weight" }}
+```
+
+Note:
+
+- The safeguards at the top allow to get rid of an unneccessary indentation level
+- Here we only moved the 'Weight' code to a partial since this is the only
+ EE-specific code in that view, so it's the most likely to conflict, but you
+ are encouraged to use partials even for code that's in CE to logically split
+ big views into several smaller files.
+
+#### Indentation issue
+
+Sometimes a code block is indented more or less in EE because there's an
+additional condition.
+
+##### Mitigations
+
+Blocks of code that are EE-specific should be moved to partials as much as
+possible to avoid conflicts with big chunks of HAML code that that are not fun
+to resolve when you add the indentation in the equation.
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 61b0fbc89c9..fd8335d251e 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -60,7 +60,7 @@ migration was tested.
If you need to remove index, please add a condition like in following example:
-```
+```ruby
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
```
@@ -75,7 +75,7 @@ need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
-```
+```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -96,7 +96,7 @@ the `up` and `down` methods in your migration class.
For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following:
-```
+```ruby
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
@@ -125,7 +125,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to
Rails migration example:
-```
+```ruby
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
# or
@@ -145,7 +145,7 @@ Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of usin
Example with Arel:
-```
+```ruby
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
@@ -154,7 +154,7 @@ users.group(users[:user_id]).having(users[:id].count.gt(5))
Example with plain SQL and `quote_string` helper:
-```
+```ruby
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 65cdd74bdb6..73893f9dd46 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -129,7 +129,7 @@ Various methods for opening and reading files in Ruby can be used to read the
standard output of a process instead of a file. The following two commands do
roughly the same:
-```
+```ruby
`touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read')
```
@@ -142,7 +142,7 @@ attacker cannot control the start of the filename string you are opening. For
instance, the following is sufficient to protect against accidentally starting
a shell command with `|`:
-```
+```ruby
# we assume repo_path is not controlled by the attacker (user)
path = File.join(repo_path, user_input)
# path cannot start with '|' now.
@@ -160,7 +160,7 @@ Path traversal is a security where the program (GitLab) tries to restrict user
access to a certain directory on disk, but the user manages to open a file
outside that directory by taking advantage of the `../` path notation.
-```
+```ruby
# Suppose the user gave us a path and they are trying to trick us
user_input = '../other-repo.git/other-file'
@@ -177,7 +177,7 @@ File.open(full_path) do # Oops!
A good way to protect against this is to compare the full path with its
'absolute path' according to Ruby's `File.absolute_path`.
-```
+```ruby
full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path)
raise "Invalid path: #{full_path.inspect}"
diff --git a/doc/development/testing.md b/doc/development/testing.md
index b0b26ccf57a..6106e47daa0 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -64,11 +64,13 @@ the command line via `bundle exec teaspoon`, or via a web browser at
methods.
- Use `context` to test branching logic.
- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)).
+- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- Prefer `not_to` to `to_not` (_this is enforced by Rubocop_).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
+- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
new file mode 100644
index 00000000000..62ac56a6bce
--- /dev/null
+++ b/doc/development/ux_guide/basics.md
@@ -0,0 +1,94 @@
+# Basics
+
+## Contents
+* [Responsive](#responsive)
+* [Typography](#typography)
+* [Icons](#icons)
+* [Color](#color)
+* [Motion](#motion)
+* [Voice and tone](#voice-and-tone)
+
+---
+
+## Responsive
+GitLab is a responsive experience that works well across all screen sizes, from mobile devices to large monitors. In order to provide a great user experience, the core functionality (browsing files, creating issues, writing comments, etc.) must be available at all resolutions. However, due to size limitations, some secondary functionality may be hidden on smaller screens. Please keep this functionality limited to rare actions that aren't expected to be needed on small devices.
+
+---
+
+## Typography
+### Primary typeface
+GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support both the bold and regular weight.
+
+![Source Sans Pro sample](img/sourcesanspro-sample.png)
+
+
+### Monospace typeface
+This is the typeface used for code blocks. GitLab uses the OS default font.
+- **Menlo** (Mac)
+- **Consolas** (Windows)
+- **Liberation Mono** (Linux)
+
+![Monospace font sample](img/monospacefont-sample.png)
+
+---
+
+## Icons
+GitLab uses Font Awesome icons throughout our interface.
+
+![Trash icon](img/icon-trash.png)
+The trash icon is used for destructive actions that deletes information.
+
+![Edit icon](img/icon-edit.png)
+The pencil icon is used for editing content such as comments.
+
+![Notification icon](img/icon-notification.png)
+The bell icon is for notifications, such as Todos.
+
+![Subscribe icon](img/icon-subscribe.png)
+The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label.
+
+![RSS icon](img/icon-rss.png)
+The standard RSS icon is used for linking to RSS/atom feeds.
+
+![Close icon](img/icon-close.png)
+An 'x' is used for closing UI elements such as dropdowns.
+
+![Add icon](img/icon-add.png)
+A plus is used when creating new objects, such as issues, projects, etc.
+
+> TODO: update this section, add more general guidance to icon usage and personality, etc.
+
+---
+
+## Color
+
+![Blue](img/color-blue.png)
+Blue is used to highlight primary active elements (such as current tab), as well as other organization and managing commands.
+
+![Green](img/color-green.png)
+Green is for actions that create new objects.
+
+![Orange](img/color-orange.png)
+Orange is used for warnings
+
+![Red](img/color-red.png)
+Red is reserved for delete and other destructive commands
+
+![Grey](img/color-grey.png)
+Grey, and white (depending on context) is used for netral, secondary elements
+
+> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
+
+---
+
+## Motion
+
+Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
+
+> TODO: Determine a more concrete perspective on motion, create consistent easing/timing curves to follow.
+
+---
+
+## Voice and tone
+
+The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
new file mode 100644
index 00000000000..764c3355714
--- /dev/null
+++ b/doc/development/ux_guide/components.md
@@ -0,0 +1,254 @@
+# Components
+
+## Contents
+* [Tooltips](#tooltips)
+* [Anchor links](#anchor-links)
+* [Buttons](#buttons)
+* [Dropdowns](#dropdowns)
+* [Counts](#counts)
+* [Lists](#lists)
+* [Tables](#tables)
+* [Blocks](#blocks)
+* [Panels](#panels)
+* [Alerts](#alerts)
+* [Forms](#forms)
+* [File holders](#file-holders)
+* [Data formats](#data-formats)
+
+---
+
+## Tooltips
+
+### Usage
+A tooltip should only be added if additional information is required.
+
+![Tooltip usage](img/tooltip-usage.png)
+
+### Placement
+By default, tooltips should be placed below the element that they refer to. However, if there is not enough space in the viewpoint, the tooltip should be moved to the side as needed.
+
+![Tooltip placement location](img/tooltip-placement.png)
+
+---
+
+## Anchor links
+
+Anchor links are used for navigational actions and lone, secondary commands (such as 'Reset filters' on the Issues List) when deemed appropriate by the UX team.
+
+### States
+
+#### Rest
+
+Primary links are blue in their rest state. Secondary links (such as the time stamp on comments) are a neutral gray color in rest. Details on the main GitLab navigation links can be found on the [features](features.md#navigation) page.
+
+#### Hover
+
+An underline should always be added on hover. A gray link becomes blue on hover.
+
+#### Focus
+
+The focus state should match the hover state.
+
+![Anchor link states ](img/components-anchorlinks.png)
+
+---
+
+## Buttons
+
+Buttons communicate the command that will occur when the user clicks on them.
+
+### Types
+
+#### Primary
+Primary buttons communicate the main call to action. There should only be one call to action in any given experience. Visually, primary buttons are conveyed with a full background fill
+
+![Primary button example](img/button-primary.png)
+
+#### Secondary
+Secondary buttons are for alternative commands. They should be conveyed by a button with an stroke, and no background fill.
+
+![Secondary button example](img/button-secondary.png)
+
+### Icon and text treatment
+Text should be in sentence case, where only the first word is capitalized. "Create issue" is correct, not "Create Issue". Buttons should only contain an icon or a text, not both.
+
+>>>
+TODO: Rationalize this. Ensure that we still believe this.
+>>>
+
+### Colors
+Follow the color guidance on the [basics](basics.md#color) page. The default color treatment is the white/grey button.
+
+---
+
+## Dropdowns
+
+Dropdowns are used to allow users to choose one (or many) options from a list of options. If this list of options is more 20, there should generally be a way to search through and filter the options (see the complex filter dropdowns below.)
+
+>>>
+TODO: Will update this section when the new filters UI is implemented.
+>>>
+
+![Dropdown states](img/components-dropdown.png)
+
+
+
+---
+
+## Counts
+
+A count element is used in navigation contexts where it is helpful to indicate the count, or number of items, in a list. Always use the [`number_with_delimiter`][number_with_delimiter] helper to display counts in the UI.
+
+![Counts example](img/components-counts.png)
+
+[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
+
+---
+
+## Lists
+
+Lists are used where ever there is a single column of information to display. Ths [issues list](https://gitlab.com/gitlab-org/gitlab-ce/issues) is an example of a important list in the GitLab UI.
+
+### Types
+
+Simple list using .content-list
+
+![Simple list](img/components-simplelist.png)
+
+List with avatar, title and description using .content-list
+
+![List with avatar](img/components-listwithavatar.png)
+
+List with hover effect .well-list
+
+![List with hover effect](img/components-listwithhover.png)
+
+List inside panel
+
+![List inside panel](img/components-listinsidepanel.png)
+
+---
+
+## Tables
+
+When the information is too complex for a list, with multiple columns of information, a table can be used. For example, the [pipelines page](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) uses a table.
+
+![Table](img/components-table.png)
+
+---
+
+## Blocks
+
+Blocks are a way to group related information.
+
+### Types
+
+#### Content blocks
+
+Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a botton border.
+
+![Content block](img/components-contentblock.png)
+
+#### Row content blocks
+
+A background color can be added to this blocks. For example, items in the [issue list](https://gitlab.com/gitlab-org/gitlab-ce/issues) have a green background if they were created recently. Below is an example of a gray content block with side padding using `.row-content-block`.
+
+![Row content block](img/components-rowcontentblock.png)
+
+#### Cover blocks
+Cover blocks are generally used to create a heading element for a page, such as a new project, or a user profile page. Below is a cover block (`.cover-block`) for the profile page with an avatar, name and description.
+
+![Cover block](img/components-coverblock.png)
+
+---
+
+## Panels
+
+>>>
+TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
+>>>
+
+![Panels](img/components-panels.png)
+
+---
+
+## Alerts
+
+>>>
+TODO: Catalog how we are currently using alerts
+>>>
+
+![Alerts](img/components-alerts.png)
+
+---
+
+## Forms
+
+There are two options shown below regarding the positioning of labels in forms. Both are options to consider based on context and available size. However, it is important to have a consistent treatment of labels in the same form.
+
+### Types
+
+#### Labels stack vertically
+
+Form (`form`) with label rendered above input.
+
+![Vertical form](img/components-verticalform.png)
+
+#### Labels side-by-side
+
+Horizontal form (`form.horizontal-form`) with label rendered inline with input.
+
+![Horizontal form](img/components-horizontalform.png)
+
+---
+
+## File holders
+A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab.
+
+![File Holder component](img/components-fileholder.png)
+
+---
+
+## Data formats
+
+### Dates
+
+#### Exact
+
+Format for exacts dates should be ‘Mon DD, YYYY’, such as the examples below.
+
+![Exact date](img/components-dateexact.png)
+
+#### Relative
+
+This format relates how long since an action has occurred. The exact date can be shown as a tooltip on hover.
+
+![Relative date](img/components-daterelative.png)
+
+### References
+
+Referencing GitLab items depends on a symbol for each type of item. Typing that symbol will invoke a dropdown that allows you to search for and autocomplete the item you were looking for. References are shown as [links](#links) in context, and hovering on them shows the full title or name of the item.
+
+![Hovering on a reference](img/components-referencehover.png)
+
+#### `%` Milestones
+
+![Milestone reference](img/components-referencemilestone.png)
+
+#### `#` Issues
+
+![Issue reference](img/components-referenceissues.png)
+
+#### `!` Merge Requests
+
+![Merge request reference](img/components-referencemrs.png)
+
+#### `~` Labels
+
+![Labels reference](img/components-referencelabels.png)
+
+#### `@` People
+
+![People reference](img/components-referencepeople.png)
+
+> TODO: Open issue: Some commit references use monospace fonts, but others don't. Need to standardize this.
diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md
new file mode 100644
index 00000000000..227b4fd3451
--- /dev/null
+++ b/doc/development/ux_guide/copy.md
@@ -0,0 +1,99 @@
+# Copy
+
+The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
+
+>**Note:**
+We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
+
+## Contents
+* [Brevity](#brevity)
+* [Sentence case](#sentence-case)
+* [Terminology](#terminology)
+
+---
+
+## Brevity
+Users will skim content, rather than read text carefully.
+When familiar with a web app, users rely on muscle memory, and may read even less when moving quickly.
+A good experience should quickly orient a user, regardless of their experience, to the purpose of the current screen. This should happen without the user having to consciously read long strings of text.
+In general, text is burdensome and adds cognitive load. This is especially pronounced in a powerful productivity tool such as GitLab.
+We should _not_ rely on words as a crutch to explain the purpose of a screen.
+The current navigation and composition of the elements on the screen should get the user 95% there, with the remaining 5% being specific elements such as text.
+This means that, as a rule, copy should be very short. A long message or label is a red flag hinting at design that needs improvement.
+
+>**Example:**
+Use `Add` instead of `Add issue` as a button label.
+Preferrably use context and placement of controls to make it obvious what clicking on them will do.
+
+---
+
+## Sentence case
+Use sentence case for all titles, headings, labels, menu items, and buttons.
+
+---
+
+## Terminology
+Only use the terms in the tables below.
+
+### Issues
+
+#### Adjectives (states)
+
+| Term |
+| ---- |
+| Open |
+| Closed |
+| Deleted |
+
+>**Example:**
+Use `5 open issues` and don't use `5 pending issues`.
+
+#### Verbs (actions)
+
+| Term | Use | Don't |
+| ---- | --- | --- |
+| Add | Add an issue | Don't use `create` or `new` |
+| View | View an open or closed issue ||
+| Edit | Edit an open or closed issue | Don't use `update` |
+| Close | Close an open issue ||
+| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb |
+| Delete | Delete an open or closed issue ||
+
+#### Add issue
+
+When viewing a list of issues, there is a button that is labeled `Add`. Given the context in the example, it is clearly referring to issues. If the context were not clear enough, the label could be `Add issue`. Clicking the button will bring you to the `Add issue` form. Other add flows should be similar.
+
+![Add issue button](img/copy-form-addissuebutton.png)
+
+The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don't use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+
+![Add issue form](img/copy-form-addissueform.png)
+
+#### Edit issue
+
+When in context of an issue, the affordance to edit it is labeled `Edit`. If the context is not clear enough, `Edit issue` could be considered. Other edit flows should be similar.
+
+![Edit issue button](img/copy-form-editissuebutton.png)
+
+The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don't use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
+
+![Edit issue form](img/copy-form-editissueform.png)
+
+
+### Merge requests
+
+#### Adjectives (states)
+
+| Term |
+| ---- |
+| Open |
+| Merged |
+
+#### Verbs (actions)
+
+| Term | Use | Don't |
+| ---- | --- | --- |
+| Add | Add a merge request | Do not use `create` or `new` |
+| View | View an open or merged merge request ||
+| Edit | Edit an open or merged merge request| Do not use `update` |
+| Merge | Merge an open merge request || \ No newline at end of file
diff --git a/doc/development/ux_guide/features.md b/doc/development/ux_guide/features.md
new file mode 100644
index 00000000000..9472995c68c
--- /dev/null
+++ b/doc/development/ux_guide/features.md
@@ -0,0 +1,57 @@
+# Features
+
+## Contents
+* [Navigation](#navigation)
+* [Filtering](#filtering)
+* [Search results](#search-results)
+* [Conversations](#conversations)
+* [Empty states](#empty-states)
+
+---
+
+## Navigation
+
+### Global navigation
+
+The global navigation is accessible via the menu button on the top left of the screen, and can be pinned to keep it open. It contains a consistent list of pages that allow you to view content that is across GitLab. For example, you can view your todos, issues and merge requests across projects and groups.
+
+![Global nav](img/features-globalnav.png)
+
+
+### Contextual navigation
+
+The navigation in the header is contextual to each page. These options change depending on if you are looking at a project, group, or settings page. There should be no more than 10 items on a level in the contextual navigation, allowing it to comfortably fit on a typical laptop screen. There can be up to too levels of navigation. Each sub nav group should be a self-contained group of functionality. For example, everything related to the issue tracker should be under the 'Issue' tab, while everything relating to the wiki will be grouped under the 'Wiki' tab. The names used for each section should be short and easy to remember, ideally 1-2 words in length.
+
+![Contextual nav](img/features-contextualnav.png)
+
+### Information architecture
+
+The [GitLab Product Map](https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png) shows a visual representation of the information architecture for GitLab.
+
+---
+
+## Filtering
+
+Today, lists are filtered by a series of dropdowns. Some of these dropdowns allow multiselect (labels), while others allow you to filter to one option (milestones). However, we are currently implementing a [new model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747) for this, and will update the guide when it is ready.
+
+![Filters](img/features-filters.png)
+
+---
+
+## Search results
+
+### Global search
+
+[Global search](https://gitlab.com/search?group_id=&project_id=13083&repository_ref=&scope=issues&search=mobile) allows you to search across items in a project, or even across multiple projects. You can switch tabs to filter on type of object, or filter by group.
+
+### List search
+
+There are several core lists in the GitLab experience, such as the Issue list and the Merge Request list. You are also able to [filter and search these lists](https://gitlab.com/gitlab-org/gitlab-ce/issues?utf8=%E2%9C%93&search=mobile). This UI will be updated with the [new filtering model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747).
+
+---
+
+## Empty states
+
+Empty states need to be considered in the design of features. They are vital to helping onboard new users, making the experience feel more approachable and understandable. Empty states should feel inviting and provide just enough information to get people started. There should be a single call to action and a clear explanation of what to use the feature for.
+
+![Empty states](img/features-emptystates.png)
diff --git a/doc/development/ux_guide/img/button-primary.png b/doc/development/ux_guide/img/button-primary.png
new file mode 100644
index 00000000000..f4c673f5b88
--- /dev/null
+++ b/doc/development/ux_guide/img/button-primary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/button-secondary.png b/doc/development/ux_guide/img/button-secondary.png
new file mode 100644
index 00000000000..57fa65b247c
--- /dev/null
+++ b/doc/development/ux_guide/img/button-secondary.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png
new file mode 100644
index 00000000000..6449613eb16
--- /dev/null
+++ b/doc/development/ux_guide/img/color-blue.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png
new file mode 100644
index 00000000000..15475b36f02
--- /dev/null
+++ b/doc/development/ux_guide/img/color-green.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-grey.png b/doc/development/ux_guide/img/color-grey.png
new file mode 100644
index 00000000000..58c474d5ce9
--- /dev/null
+++ b/doc/development/ux_guide/img/color-grey.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png
new file mode 100644
index 00000000000..f4fc09b2d9b
--- /dev/null
+++ b/doc/development/ux_guide/img/color-orange.png
Binary files differ
diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png
new file mode 100644
index 00000000000..6fbbf0a885d
--- /dev/null
+++ b/doc/development/ux_guide/img/color-red.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-alerts.png b/doc/development/ux_guide/img/components-alerts.png
new file mode 100644
index 00000000000..0b2ecc16a5f
--- /dev/null
+++ b/doc/development/ux_guide/img/components-alerts.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png
new file mode 100644
index 00000000000..950f348277d
--- /dev/null
+++ b/doc/development/ux_guide/img/components-anchorlinks.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-contentblock.png b/doc/development/ux_guide/img/components-contentblock.png
new file mode 100644
index 00000000000..31fc1eec9df
--- /dev/null
+++ b/doc/development/ux_guide/img/components-contentblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-counts.png b/doc/development/ux_guide/img/components-counts.png
new file mode 100644
index 00000000000..19280e988a0
--- /dev/null
+++ b/doc/development/ux_guide/img/components-counts.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-coverblock.png b/doc/development/ux_guide/img/components-coverblock.png
new file mode 100644
index 00000000000..c8f1f87a108
--- /dev/null
+++ b/doc/development/ux_guide/img/components-coverblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dateexact.png b/doc/development/ux_guide/img/components-dateexact.png
new file mode 100644
index 00000000000..8c0c5c1be40
--- /dev/null
+++ b/doc/development/ux_guide/img/components-dateexact.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-daterelative.png b/doc/development/ux_guide/img/components-daterelative.png
new file mode 100644
index 00000000000..1dc6d89e4ef
--- /dev/null
+++ b/doc/development/ux_guide/img/components-daterelative.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dropdown.png b/doc/development/ux_guide/img/components-dropdown.png
new file mode 100644
index 00000000000..5770a393b37
--- /dev/null
+++ b/doc/development/ux_guide/img/components-dropdown.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-fileholder.png b/doc/development/ux_guide/img/components-fileholder.png
new file mode 100644
index 00000000000..4b8962905d6
--- /dev/null
+++ b/doc/development/ux_guide/img/components-fileholder.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-horizontalform.png b/doc/development/ux_guide/img/components-horizontalform.png
new file mode 100644
index 00000000000..92e28cf9afc
--- /dev/null
+++ b/doc/development/ux_guide/img/components-horizontalform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listinsidepanel.png b/doc/development/ux_guide/img/components-listinsidepanel.png
new file mode 100644
index 00000000000..30ceb3eaa08
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listinsidepanel.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithavatar.png b/doc/development/ux_guide/img/components-listwithavatar.png
new file mode 100644
index 00000000000..d3cb0ebc02b
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listwithavatar.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithhover.png b/doc/development/ux_guide/img/components-listwithhover.png
new file mode 100644
index 00000000000..1484ecba6a0
--- /dev/null
+++ b/doc/development/ux_guide/img/components-listwithhover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-panels.png b/doc/development/ux_guide/img/components-panels.png
new file mode 100644
index 00000000000..6e71d0ad9c9
--- /dev/null
+++ b/doc/development/ux_guide/img/components-panels.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencehover.png b/doc/development/ux_guide/img/components-referencehover.png
new file mode 100644
index 00000000000..e9fb27e2aa9
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencehover.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referenceissues.png b/doc/development/ux_guide/img/components-referenceissues.png
new file mode 100644
index 00000000000..caf9477db38
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referenceissues.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencelabels.png b/doc/development/ux_guide/img/components-referencelabels.png
new file mode 100644
index 00000000000..a122b45d1f1
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencelabels.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemilestone.png b/doc/development/ux_guide/img/components-referencemilestone.png
new file mode 100644
index 00000000000..5aa9ecd1a78
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencemilestone.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemrs.png b/doc/development/ux_guide/img/components-referencemrs.png
new file mode 100644
index 00000000000..6280243859a
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencemrs.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencepeople.png b/doc/development/ux_guide/img/components-referencepeople.png
new file mode 100644
index 00000000000..99772a539cf
--- /dev/null
+++ b/doc/development/ux_guide/img/components-referencepeople.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-rowcontentblock.png b/doc/development/ux_guide/img/components-rowcontentblock.png
new file mode 100644
index 00000000000..1c2d7096955
--- /dev/null
+++ b/doc/development/ux_guide/img/components-rowcontentblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-simplelist.png b/doc/development/ux_guide/img/components-simplelist.png
new file mode 100644
index 00000000000..892f507cfc2
--- /dev/null
+++ b/doc/development/ux_guide/img/components-simplelist.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-table.png b/doc/development/ux_guide/img/components-table.png
new file mode 100644
index 00000000000..7e964c885cf
--- /dev/null
+++ b/doc/development/ux_guide/img/components-table.png
Binary files differ
diff --git a/doc/development/ux_guide/img/components-verticalform.png b/doc/development/ux_guide/img/components-verticalform.png
new file mode 100644
index 00000000000..38863ad3c1c
--- /dev/null
+++ b/doc/development/ux_guide/img/components-verticalform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissuebutton.png b/doc/development/ux_guide/img/copy-form-addissuebutton.png
new file mode 100644
index 00000000000..18839d447e8
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-addissueform.png b/doc/development/ux_guide/img/copy-form-addissueform.png
new file mode 100644
index 00000000000..e6838c06eca
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-addissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissuebutton.png b/doc/development/ux_guide/img/copy-form-editissuebutton.png
new file mode 100644
index 00000000000..2435820e14f
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissuebutton.png
Binary files differ
diff --git a/doc/development/ux_guide/img/copy-form-editissueform.png b/doc/development/ux_guide/img/copy-form-editissueform.png
new file mode 100644
index 00000000000..5ddeda33e68
--- /dev/null
+++ b/doc/development/ux_guide/img/copy-form-editissueform.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-contextualnav.png b/doc/development/ux_guide/img/features-contextualnav.png
new file mode 100644
index 00000000000..df157f54c84
--- /dev/null
+++ b/doc/development/ux_guide/img/features-contextualnav.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-emptystates.png b/doc/development/ux_guide/img/features-emptystates.png
new file mode 100644
index 00000000000..3befc14588e
--- /dev/null
+++ b/doc/development/ux_guide/img/features-emptystates.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-filters.png b/doc/development/ux_guide/img/features-filters.png
new file mode 100644
index 00000000000..281e55d590c
--- /dev/null
+++ b/doc/development/ux_guide/img/features-filters.png
Binary files differ
diff --git a/doc/development/ux_guide/img/features-globalnav.png b/doc/development/ux_guide/img/features-globalnav.png
new file mode 100644
index 00000000000..3c0db2247ca
--- /dev/null
+++ b/doc/development/ux_guide/img/features-globalnav.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-add.png b/doc/development/ux_guide/img/icon-add.png
new file mode 100644
index 00000000000..0d4c1a7692a
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-add.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-close.png b/doc/development/ux_guide/img/icon-close.png
new file mode 100644
index 00000000000..88d2b3b0c6d
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-close.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-edit.png b/doc/development/ux_guide/img/icon-edit.png
new file mode 100644
index 00000000000..f73be7a10fb
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-edit.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-notification.png b/doc/development/ux_guide/img/icon-notification.png
new file mode 100644
index 00000000000..4758632edd7
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-notification.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-rss.png b/doc/development/ux_guide/img/icon-rss.png
new file mode 100644
index 00000000000..c7ac9fb1349
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-rss.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-subscribe.png b/doc/development/ux_guide/img/icon-subscribe.png
new file mode 100644
index 00000000000..5cb277bfd5d
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-subscribe.png
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-trash.png b/doc/development/ux_guide/img/icon-trash.png
new file mode 100644
index 00000000000..357289a6fff
--- /dev/null
+++ b/doc/development/ux_guide/img/icon-trash.png
Binary files differ
diff --git a/doc/development/ux_guide/img/monospacefont-sample.png b/doc/development/ux_guide/img/monospacefont-sample.png
new file mode 100644
index 00000000000..1cd290b713c
--- /dev/null
+++ b/doc/development/ux_guide/img/monospacefont-sample.png
Binary files differ
diff --git a/doc/development/ux_guide/img/sourcesanspro-sample.png b/doc/development/ux_guide/img/sourcesanspro-sample.png
new file mode 100644
index 00000000000..f7ecf0c7c66
--- /dev/null
+++ b/doc/development/ux_guide/img/sourcesanspro-sample.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-contentitemtitle.png b/doc/development/ux_guide/img/surfaces-contentitemtitle.png
new file mode 100644
index 00000000000..2eb926c1c43
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-contentitemtitle.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-header.png b/doc/development/ux_guide/img/surfaces-header.png
new file mode 100644
index 00000000000..ab44d4de696
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-header.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-systeminformationblock.png b/doc/development/ux_guide/img/surfaces-systeminformationblock.png
new file mode 100644
index 00000000000..5d91e993e24
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-systeminformationblock.png
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-ux.png b/doc/development/ux_guide/img/surfaces-ux.png
new file mode 100644
index 00000000000..e692c51e8c0
--- /dev/null
+++ b/doc/development/ux_guide/img/surfaces-ux.png
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-placement.png b/doc/development/ux_guide/img/tooltip-placement.png
new file mode 100644
index 00000000000..29a61c8400a
--- /dev/null
+++ b/doc/development/ux_guide/img/tooltip-placement.png
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-usage.png b/doc/development/ux_guide/img/tooltip-usage.png
new file mode 100644
index 00000000000..e8e4c6ded91
--- /dev/null
+++ b/doc/development/ux_guide/img/tooltip-usage.png
Binary files differ
diff --git a/doc/development/ux_guide/index.md b/doc/development/ux_guide/index.md
new file mode 100644
index 00000000000..8aed11ebac3
--- /dev/null
+++ b/doc/development/ux_guide/index.md
@@ -0,0 +1,58 @@
+# GitLab UX Guide
+
+The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions.
+
+## Design
+
+---
+
+### [Principles](principles.md)
+These guiding principles set a solid foundation for our design system, and should remain relatively stable over multiple releases. They should be referenced as new design patterns are created.
+
+---
+
+### [Basics](basics.md)
+The basic ingredients of our experience establish our personality and feel. This section includes details about typography, color, and motion.
+
+---
+
+### [Components](components.md)
+Components are the controls that make up the GitLab experience, including guidance around buttons, links, dropdowns, etc.
+
+---
+
+### [Surfaces](surfaces.md)
+The GitLab experience is broken apart into several surfaces. Each of these surfaces is designated for a specific scope or type of content. Examples include the header, global menu, side pane, etc.
+
+---
+
+### [Copy](copy.md)
+Conventions on text and messaging within labels, buttons, and other components.
+
+---
+
+### [Features](features.md)
+The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states.
+
+---
+
+## Research
+
+---
+
+### [Users](users.md)
+How we think about the variety of users of GitLab, from small to large teams, comparing opensource usage to enterprise, etc.
+
+---
+
+## Other
+
+---
+
+### [Tips for designers](tips.md)
+Tips for exporting assets, and other guidance.
+
+---
+
+### [Resources](resources.md)
+Resources for GitLab UX
diff --git a/doc/development/ux_guide/principles.md b/doc/development/ux_guide/principles.md
new file mode 100644
index 00000000000..1a297cba2cc
--- /dev/null
+++ b/doc/development/ux_guide/principles.md
@@ -0,0 +1,17 @@
+# Principles
+
+These are the guiding principles that we should strive for to establish a solid foundation for the GitLab experience.
+
+## Professional and productive
+GitLab is a tool to support what people do, day in, day out. We need to respect the importance of their work, and avoid gimicky details.
+
+## Minimal and efficient
+While work can get complicated, GitLab is about bringing a sharp focus, helping our customers know what matters now.
+
+## Immediately recognizable
+When you look at any screen, you should know immediately that it is GitLab. Our personality is strong and consistent across product and marketing experiences.
+
+## Human and quirky
+We need to build empathy with our users, understanding their state of mind, and connect with them at a human level. Quirkiness is part of our DNA, and we should embrace it in the right moments and contexts.
+
+> TODO: Ensure these principles align well with the goals of the Marketing team
diff --git a/doc/development/ux_guide/resources.md b/doc/development/ux_guide/resources.md
new file mode 100644
index 00000000000..2f760c94414
--- /dev/null
+++ b/doc/development/ux_guide/resources.md
@@ -0,0 +1,13 @@
+# Resources
+
+## GitLab UI development kit
+
+We created a page inside GitLab where you can check commonly used html and css elements.
+
+When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples
+you can use during GitLab development.
+
+## Design repository
+
+All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
+repository and maintained by GitLab UX designers. \ No newline at end of file
diff --git a/doc/development/ux_guide/surfaces.md b/doc/development/ux_guide/surfaces.md
new file mode 100644
index 00000000000..881d6aa4cd6
--- /dev/null
+++ b/doc/development/ux_guide/surfaces.md
@@ -0,0 +1,47 @@
+# Surfaces
+
+## Contents
+* [Header](#header)
+* [Global menu](#global-menu)
+* [Side pane](#side-pane)
+* [Content area](#content-area)
+
+---
+
+![Surfaces UX](img/surfaces-ux.png)
+
+## Global menu
+
+This menu is to navigate to pages that contain content global to GitLab.
+
+---
+
+## Header
+
+The header contains 3 main elements: Project switching and searching, user account avatar and settings, and a contextual menu that changes based on the current page.
+
+![Surfaces Header](img/surfaces-header.png)
+
+---
+
+## Side pane
+
+The side pane holds supporting information and meta data for the information in the content area.
+
+---
+
+## Content area
+
+The main content of the page. The content area can include other surfaces.
+
+### Item title bar
+
+The item title bar contains the top level information to identify the item, such as the name, id and status.
+
+![Item title](img/surfaces-contentitemtitle.png)
+
+### Item system information
+
+The system information block contains relevant system controlled information.
+
+![Item system information](img/surfaces-systeminformationblock.png)
diff --git a/doc/development/ux_guide/tips.md b/doc/development/ux_guide/tips.md
new file mode 100644
index 00000000000..8348de4f8a2
--- /dev/null
+++ b/doc/development/ux_guide/tips.md
@@ -0,0 +1,44 @@
+# Tips
+
+## Contents
+* [SVGs](#svgs)
+
+---
+
+## SVGs
+
+When exporting SVGs, be sure to follow the following guidelines:
+
+1. Convert all strokes to outlines.
+2. Use pathfinder tools to combine overlapping paths and create compound paths.
+3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
+4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
+
+You can open your svg in a text editor to ensure that it is clean.
+Incorrect files will look like this:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
+ <title>Group</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#7E7C7C">
+ <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
+ <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
+ <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
+ <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
+ </g>
+ </g>
+</svg>
+```
+
+Correct file will look like this:
+
+```xml
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
+```
+
+> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo)
diff --git a/doc/development/ux_guide/users.md b/doc/development/ux_guide/users.md
new file mode 100644
index 00000000000..717a902c424
--- /dev/null
+++ b/doc/development/ux_guide/users.md
@@ -0,0 +1,16 @@
+# Users
+
+> TODO: Create personas. Understand the similarities and differences across the below spectrums.
+
+## Users by organization
+
+- Enterprise
+- Medium company
+- Small company
+- Open source communities
+
+## Users by role
+
+- Admin
+- Manager
+- Developer
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 3f45a631b3a..1c549844ee1 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -14,8 +14,8 @@ There are two ways to create a new project in GitLab.
1. Fill out the information:
- 1. "Project name" is the name of your project (you can't use spaces, but you
- can use hyphens or underscores).
+ 1. "Project name" is the name of your project (you can't use special characters,
+ but you can use spaces, hyphens, underscores or even emojis).
1. The "Project description" is optional and will be shown in your project's
dashboard so others can briefly understand what your project is about.
1. Select a [visibility level](../public_access/public_access.md).
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 766a7119943..42b515761e0 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -143,9 +143,6 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
## Supported web browsers
-- Chrome (Latest stable version)
-- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
-- Safari 7+ (known problem: required fields in html5 do not work)
-- Opera (Latest released version)
-- Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled.
-- Edge (Latest stable version)
+We support the current and the previous major release of Firefox, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
+
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
diff --git a/doc/integration/README.md b/doc/integration/README.md
index a928b74f9b8..ae4387e2577 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,7 +5,7 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [JIRA](jira.md) Integrate with the JIRA issue tracker
+- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
@@ -44,11 +44,15 @@ This [resource](http://kb.kerio.com/product/kerio-connect/server-configuration/s
has all the information you need to add a certificate to the main trusted chain.
This [answer](http://superuser.com/questions/437330/how-do-you-add-a-certificate-authority-ca-to-ubuntu)
-at SuperUser also has relevant information.
+at Super User also has relevant information.
**Omnibus Trusted Chain**
-It is enough to concatenate the certificate to the main trusted certificate:
+[Install the self signed certificate or custom certificate authorities](http://docs.gitlab.com/omnibus/common_installation_problems/README.html#using-self-signed-certificate-or-custom-certificate-authorities)
+in to GitLab Omnibus.
+
+It is enough to concatenate the certificate to the main trusted certificate
+however it may be overwritten during upgrades:
```bash
cat jira.pem >> /opt/gitlab/embedded/ssl/certs/cacert.pem
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index 2e31fd994de..78aa6634116 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,197 +1,3 @@
# GitLab JIRA integration
-GitLab can be configured to interact with JIRA. Configuration happens via
-user name and password. Connecting to a JIRA server via CAS is not possible.
-
-Each project can be configured to connect to a different JIRA instance, see the
-[configuration](#configuration) section. If you have one JIRA instance you can
-pre-fill the settings page with a default template. To configure the template
-see the [Services Templates][services-templates] document.
-
-Once the project is connected to JIRA, you can reference and close the issues
-in JIRA directly from GitLab.
-
-## Table of Contents
-* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues)
-* [Closing JIRA Issues from GitLab](#closing-JIRA-issues)
-* [Configuration](#configuration)
-
-### Referencing JIRA Issues
-
-When GitLab project has JIRA issue tracker configured and enabled, mentioning
-JIRA issue in GitLab will automatically add a comment in JIRA issue with the
-link back to GitLab. This means that in comments in merge requests and commits
-referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the
-format:
-
-```
- USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
- ENTITY_TITLE
-```
-
-* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
-* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
-* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
-* `PROJECT_NAME` GitLab project name.
-* `ENTITY_TITLE` Merge request title or commit message first line.
-
-![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
-
----
-
-### Closing JIRA Issues
-
-JIRA issues can be closed directly from GitLab by using trigger words, eg.
-`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and
-merge requests. When a commit which contains the trigger word in the commit
-message is pushed, GitLab will add a comment in the mentioned JIRA issue.
-
-For example, for project named `PROJECT` in JIRA, we implemented a new feature
-and created a merge request in GitLab.
-
-This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab
-contains the improvement and in merge request description we say that this
-merge request `Closes PROJECT-7` issue.
-
-Once this merge request is merged, the JIRA issue will be automatically closed
-with a link to the commit that resolved the issue.
-
-![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
-
----
-
-![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png)
-
----
-
-## Configuration
-
-### Configuring JIRA
-
-We need to create a user in JIRA which will have access to all projects that
-need to integrate with GitLab. Login to your JIRA instance as admin and under
-Administration go to User Management and create a new user.
-
-As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
-group.
-
-**It is important that the user `GitLab` has write-access to projects in JIRA**
-
-We have split this stage in steps so it is easier to follow.
-
----
-
-1. Login to your JIRA instance as an administrator and under **Administration**
- go to **User Management** to create a new user.
-
- ![JIRA user management link](img/jira_user_management_link.png)
-
- ---
-
-1. The next step is to create a new user (e.g., `gitlab`) who has write access
- to projects in JIRA. Enter the user's name and a _valid_ e-mail address
- since JIRA sends a verification e-mail to set-up the password.
- _**Note:** JIRA creates the username automatically by using the e-mail
- prefix. You can change it later if you want._
-
- ![JIRA create new user](img/jira_create_new_user.png)
-
- ---
-
-1. Now, let's create a `gitlab-developers` group which will have write access
- to projects in JIRA. Go to the **Groups** tab and select **Create group**.
-
- ![JIRA create new user](img/jira_create_new_group.png)
-
- ---
-
- Give it an optional description and hit **Create group**.
-
- ![jira create new group](img/jira_create_new_group_name.png)
-
- ---
-
-1. Give the newly-created group write access by going to
- **Application access > View configuration** and adding the `gitlab-developers`
- group to JIRA Core.
-
- ![JIRA group access](img/jira_group_access.png)
-
- ---
-
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users > GitLab user > Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_ which is
- what we aim for.
-
- ![JIRA add user to group](img/jira_add_user_to_group.png)
-
----
-
-The JIRA configuration is over. Write down the new JIRA username and its
-password as they will be needed when configuring GitLab in the next section.
-
-### Configuring GitLab
-
-JIRA configuration in GitLab is done via a project's **Services**.
-
-#### GitLab 8.13.0 with JIRA v1000.x
-
-To enable JIRA integration in a project, navigate to the project's
-and open the context menu clicking on the top right gear icon, then go to
-**Services > JIRA**.
-
-Fill in the required details on the page as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com |
-| `Project key` | The short, all capital letter identifier for your JIRA project. |
-| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). |
-| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). |
-| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
-
-After saving the configuration, your GitLab project will be able to interact
-with the linked JIRA project.
-
-![JIRA service page](img/jira_service_page.png)
-
----
-
-#### GitLab 6.x-7.7 with JIRA v6.x
-
-_**Note:** GitLab versions 8.13.0 and up contain various integration improvements.
-We strongly recommend upgrading._
-
-In `gitlab.yml` enable the JIRA issue tracker section by
-[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all
-issues within GitLab are pointing to the JIRA issue tracker.
-
-After you set this, you will be able to close issues in JIRA by a commit in
-GitLab.
-
-Go to your project's **Settings** page and fill in the project name for the
-JIRA project:
-
-![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png)
-
----
-
-You can also enable the JIRA service that will allow you to interact with JIRA
-issues. Go to the **Settings > Services > JIRA** and:
-
-1. Tick the active check box to enable the service
-1. Supply the URL to JIRA server, for example http://JIRA.example.com
-1. Supply the username of a user we created under `Configuring JIRA` section,
- for example `gitlab`
-1. Supply the password of the user
-1. Optional: supply the JIRA API version, default is version `2`
-1. Optional: supply the JIRA issue transition ID (issue transition to closed).
- This is dependent on JIRA settings, default is `2`
-1. Hit save
-
-
-![JIRA services page](img/jira_service.png)
-
-[services-templates]: ../project_services/services_templates.md
-[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115
+This document was moved under [project_services/jira](../project_services/jira.md).
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 5210ce0de9a..eb9bbb67e7d 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -10,7 +10,7 @@ To enable the Shibboleth OmniAuth provider you must:
1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document.
Check https://wiki.shibboleth.net/ for more info.
-1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf)
+1. You can find Apache config in gitlab-recipes (https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache)
Following changes are needed to enable shibboleth:
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
index 440728795be..88943dc410e 100644
--- a/doc/project_services/img/builds_emails_service.png
+++ b/doc/project_services/img/builds_emails_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png
new file mode 100644
index 00000000000..aec472b9118
--- /dev/null
+++ b/doc/project_services/img/jira_add_gitlab_commit_message.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png
new file mode 100644
index 00000000000..0ba737bda9a
--- /dev/null
+++ b/doc/project_services/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png
new file mode 100644
index 00000000000..0609060cb05
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png
new file mode 100644
index 00000000000..53d77b17df0
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png
new file mode 100644
index 00000000000..9eaa444ed25
--- /dev/null
+++ b/doc/project_services/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png
new file mode 100644
index 00000000000..8d4657427ae
--- /dev/null
+++ b/doc/project_services/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png
new file mode 100644
index 00000000000..acdd83702d3
--- /dev/null
+++ b/doc/project_services/img/jira_issue_closed.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png
new file mode 100644
index 00000000000..1a2d9f04a6c
--- /dev/null
+++ b/doc/project_services/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png
new file mode 100644
index 00000000000..0703081d77b
--- /dev/null
+++ b/doc/project_services/img/jira_issues_workflow.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png
new file mode 100644
index 00000000000..47785e3ba27
--- /dev/null
+++ b/doc/project_services/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png
new file mode 100644
index 00000000000..e785ec6140d
--- /dev/null
+++ b/doc/project_services/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
new file mode 100644
index 00000000000..fb270d85e3c
--- /dev/null
+++ b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png
new file mode 100644
index 00000000000..13aefce6f84
--- /dev/null
+++ b/doc/project_services/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png
new file mode 100644
index 00000000000..eed69e80d2c
--- /dev/null
+++ b/doc/project_services/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png
new file mode 100644
index 00000000000..a5b49c501ba
--- /dev/null
+++ b/doc/project_services/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png
new file mode 100644
index 00000000000..77630d39d39
--- /dev/null
+++ b/doc/project_services/img/jira_submit_gitlab_merge_request.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png
new file mode 100644
index 00000000000..5f002b59bac
--- /dev/null
+++ b/doc/project_services/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png
new file mode 100644
index 00000000000..937a50a77d9
--- /dev/null
+++ b/doc/project_services/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 2ea1c58cb31..b626c746c79 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1 +1,246 @@
-GitLab JIRA integration documentation has moved to [here](../integration/jira.md).
+# GitLab JIRA integration
+
+>**Note:**
+Full JIRA integration was previously exclusive to GitLab Enterprise Edition.
+With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce]
+to GitLab Community Edition as well.
+
+---
+
+GitLab can be configured to interact with [JIRA Core] either using an
+on-premises instance or the SaaS solution that Atlassian offers. Configuration
+happens via username and password on a per-project basis. Connecting to a JIRA
+server via CAS is not possible.
+
+Each project can be configured to connect to a different JIRA instance or, in
+case you have a single JIRA instance, you can pre-fill the JIRA service
+settings page in GitLab with a default template. To configure the JIRA template,
+see the [Services Templates documentation][services-templates].
+
+Once the GitLab project is connected to JIRA, you can reference and close the
+issues in JIRA directly from GitLab's merge requests.
+
+## Configuration
+
+The configuration consists of two parts:
+
+- [JIRA configuration](#configuring-jira)
+- [GitLab configuration](#configuring-gitlab)
+
+### Configuring JIRA
+
+First things first, we need to create a user in JIRA which will have access to
+all projects that need to integrate with GitLab.
+
+We have split this stage in steps so it is easier to follow.
+
+---
+
+1. Login to your JIRA instance as an administrator and under **Administration**
+ go to **User Management** to create a new user.
+
+ ![JIRA user management link](img/jira_user_management_link.png)
+
+ ---
+
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
+ to projects in JIRA. Enter the user's name and a _valid_ e-mail address
+ since JIRA sends a verification e-mail to set-up the password.
+ _**Note:** JIRA creates the username automatically by using the e-mail
+ prefix. You can change it later if you want._
+
+ ![JIRA create new user](img/jira_create_new_user.png)
+
+ ---
+
+1. Now, let's create a `gitlab-developers` group which will have write access
+ to projects in JIRA. Go to the **Groups** tab and select **Create group**.
+
+ ![JIRA create new user](img/jira_create_new_group.png)
+
+ ---
+
+ Give it an optional description and hit **Create group**.
+
+ ![JIRA create new group](img/jira_create_new_group_name.png)
+
+ ---
+
+1. Give the newly-created group write access by going to
+ **Application access > View configuration** and adding the `gitlab-developers`
+ group to JIRA Core.
+
+ ![JIRA group access](img/jira_group_access.png)
+
+ ---
+
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
+ **Users > GitLab user > Add group** and selecting the `gitlab-developers`
+ group from the dropdown menu. Notice that the group says _Access_ which is
+ what we aim for.
+
+ ![JIRA add user to group](img/jira_add_user_to_group.png)
+
+---
+
+The JIRA configuration is over. Write down the new JIRA username and its
+password as they will be needed when configuring GitLab in the next section.
+
+### Configuring GitLab
+
+>**Note:**
+The currently supported JIRA versions are v6.x and v7.x. and GitLab
+7.8 or higher is required.
+
+---
+
+Assuming you [have already configured JIRA](#configuring-jira), now it's time
+to configure GitLab.
+
+JIRA configuration in GitLab is done via a project's
+[**Services**](../project_services/project_services.md).
+
+To enable JIRA integration in a project, navigate to the project's
+**Settings > Services > JIRA**.
+
+Fill in the required details on the page, as described in the table below.
+
+| Setting | Description |
+| ------- | ----------- |
+| `Description` | A name for the issue tracker (to differentiate between instances, for example). |
+| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
+| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. |
+| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
+| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
+| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). |
+| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
+| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
+
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+
+For example, given the settings below:
+
+- the JIRA URL is `https://jira.example.com`
+- the project is named `GITLAB`
+- the user is named `gitlab`
+- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans])
+
+the following screenshot shows how the JIRA service settings should look like.
+
+![JIRA service page](img/jira_service_page.png)
+
+[trans]: img/jira_issues_workflow.png
+
+---
+
+## JIRA issues
+
+By now you should have [configured JIRA](#configuring-jira) and enabled the
+[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
+you should be able to reference and close JIRA issues by just mentioning their
+ID in GitLab commits and merge requests.
+
+### Referencing JIRA Issues
+
+If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link
+which points back to JIRA is created.
+
+The same works for comments in merge requests as well.
+
+![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png)
+
+---
+
+The mentioning action is two-fold, so a comment with a JIRA issue in GitLab
+will automatically add a comment in that particular JIRA issue with the link
+back to GitLab.
+
+
+![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png)
+
+---
+
+The comment on the JIRA issue is of the form:
+
+> USER mentioned this issue in LINK_TO_THE_MENTION
+
+Where:
+
+| Format | Description |
+| ------ | ----------- |
+| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. |
+| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. |
+
+### Closing JIRA issues
+
+JIRA issues can be closed directly from GitLab by using trigger words in
+commits and merge requests. When a commit which contains the trigger word
+followed by the JIRA issue ID in the commit message is pushed, GitLab will
+add a comment in the mentioned JIRA issue and immediately close it (provided
+the transition ID was set up correctly).
+
+There are currently three trigger words, and you can use either one to achieve
+the same goal:
+
+- `Resolves GITLAB-1`
+- `Closes GITLAB-1`
+- `Fixes GITLAB-1`
+
+where `GITLAB-1` the issue ID of the JIRA project.
+
+### JIRA issue closing example
+
+Let's say for example that we submitted a bug fix and created a merge request
+in GitLab. The workflow would be something like this:
+
+1. Create a new branch
+1. Fix the bug
+1. Commit the changes and push branch to GitLab
+1. Open a new merge request and reference the JIRA issue including one of the
+ trigger words, e.g.: `Fixes GITLAB-1`, in the description
+1. Submit the merge request
+1. Ask someone to review
+1. Merge the merge request
+1. The JIRA issue is automatically closed
+
+---
+
+In the following screenshot you can see what the link references to the JIRA
+issue look like.
+
+![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png)
+
+---
+
+Once this merge request is merged, the JIRA issue will be automatically closed
+with a link to the commit that resolved the issue.
+
+![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png)
+
+---
+
+You can see from the above image that there are four references to GitLab:
+
+- The first is from a comment in a specific commit
+- The second is from the JIRA issue reference in the merge request description
+- The third is from the actual commit that solved the issue
+- And the fourth is from the commit that the merge request created
+
+[services-templates]: ../project_services/services_templates.md "Services templates documentation"
+[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website"
+[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service"
+[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post"
+
+## Troubleshooting
+
+### GitLab is unable to comment on a ticket
+
+Make sure that the user you set up for GitLab to communicate with JIRA has the
+correct access permission to post comments on a ticket and to also transition the
+ticket, if you'd like GitLab to also take care of closing them.
+
+### GitLab is unable to close a ticket
+
+Make sure the the `Transition ID` you set within the JIRA settings matches the
+one your project needs to close a ticket.
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 8116a1ce976..4442b7c1742 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome.
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
-| [JIRA](../integration/jira.md) | JIRA issue tracker |
+| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
| 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 |
diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md
index 3ff3fee6a40..f7f6a40cd04 100644
--- a/doc/raketasks/check.md
+++ b/doc/raketasks/check.md
@@ -1,63 +1,3 @@
# Check Rake Tasks
-## Repository Integrity
-
-Even though Git is very resilient and tries to prevent data integrity issues,
-there are times when things go wrong. The following Rake tasks intend to
-help GitLab administrators diagnose problem repositories so they can be fixed.
-
-There are 3 things that are checked to determine integrity.
-
-1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
- This step verifies the connectivity and validity of objects in the repository.
-1. Check for `config.lock` in the repository directory.
-1. Check for any branch/references lock files in `refs/heads`.
-
-It's important to note that the existence of `config.lock` or reference locks
-alone do not necessarily indicate a problem. Lock files are routinely created
-and removed as Git and GitLab perform operations on the repository. They serve
-to prevent data integrity issues. However, if a Git operation is interrupted these
-locks may not be cleaned up properly.
-
-The following symptoms may indicate a problem with repository integrity. If users
-experience these symptoms you may use the rake tasks described below to determine
-exactly which repositories are causing the trouble.
-
-- Receiving an error when trying to push code - `remote: error: cannot lock ref`
-- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
-
-### Check all GitLab repositories
-
-This task loops through all repositories on the GitLab server and runs the
-3 integrity checks described previously.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:repo:check
-
-# installation from source
-bundle exec rake gitlab:repo:check RAILS_ENV=production
-```
-
-### Check repositories for a specific user
-
-This task checks all repositories that a specific user has access to. This is important
-because sometimes you know which user is experiencing trouble but you don't know
-which project might be the cause.
-
-If the rake task is executed without brackets at the end, you will be prompted
-to enter a username.
-
-```bash
-# omnibus-gitlab
-sudo gitlab-rake gitlab:user:check_repos
-sudo gitlab-rake gitlab:user:check_repos[<username>]
-
-# installation from source
-bundle exec rake gitlab:user:check_repos RAILS_ENV=production
-bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
-```
-
-Example output:
-
-![gitlab:user:check_repos output](check_repos_output.png)
+This document was moved to [administration/raketasks/check](../administration/raketasks/check.md).
diff --git a/doc/university/README.md b/doc/university/README.md
index 49714e4fb59..4569bc72797 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -212,5 +212,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Support Path](support/README.md)
1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [User Training](training/user_training.md)
+1. [GitLab Flow Training](training/gitlab_flow.md)
+1. [Training Topics](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/topics/)
1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/university/training/gitlab_flow.md b/doc/university/training/gitlab_flow.md
new file mode 100755
index 00000000000..a7db1f2e069
--- /dev/null
+++ b/doc/university/training/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+---
+
+# Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](gitlab_flow/feature_branches.png)
+
+---
+
+# Production branch
+
+- One, long-running production release branch
+ as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+---
+
+# Production branch
+
+![inline](gitlab_flow/production_branch.png)
+
+---
+
+# Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+ from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+---
+
+# Release branch
+
+![inline](gitlab_flow/release_branches.png)
+
+---
+
+# More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/gitlab_flow/feature_branches.png b/doc/university/training/gitlab_flow/feature_branches.png
new file mode 100644
index 00000000000..88addb623ee
--- /dev/null
+++ b/doc/university/training/gitlab_flow/feature_branches.png
Binary files differ
diff --git a/doc/university/training/gitlab_flow/production_branch.png b/doc/university/training/gitlab_flow/production_branch.png
new file mode 100644
index 00000000000..33fb26dd621
--- /dev/null
+++ b/doc/university/training/gitlab_flow/production_branch.png
Binary files differ
diff --git a/doc/university/training/gitlab_flow/release_branches.png b/doc/university/training/gitlab_flow/release_branches.png
new file mode 100644
index 00000000000..da7ae53413a
--- /dev/null
+++ b/doc/university/training/gitlab_flow/release_branches.png
Binary files differ
diff --git a/doc/university/training/index.md b/doc/university/training/index.md
new file mode 100755
index 00000000000..03179ff5a77
--- /dev/null
+++ b/doc/university/training/index.md
@@ -0,0 +1,6 @@
+# GitLab Training Material
+
+All GitLab training material is stored in markdown format. Slides are
+generated using [Deskset](http://www.decksetapp.com/).
+
+All training material is open to public contribution.
diff --git a/doc/university/training/logo.png b/doc/university/training/logo.png
new file mode 100644
index 00000000000..cc831790405
--- /dev/null
+++ b/doc/university/training/logo.png
Binary files differ
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
new file mode 100755
index 00000000000..1ee615432aa
--- /dev/null
+++ b/doc/university/training/topics/additional_resources.md
@@ -0,0 +1,8 @@
+## Additional Resources
+
+1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+5. Code School tutorial [http://try.github.io/](http://try.github.io/)
+6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
new file mode 100755
index 00000000000..e6e4fea9b51
--- /dev/null
+++ b/doc/university/training/topics/agile_git.md
@@ -0,0 +1,33 @@
+# Agile and Git
+
+----------
+
+## Agile
+
+Lean software development methods focused on collaboration and interaction
+with fast and smaller deployment cycles.
+
+----------
+
+## Where Git comes in
+
+Git is an excellent tool for an Agile team considering that it allows
+decentralized and simultaneous development.
+
+----------
+
+### Branching And Workflows
+
+Branching in an Agile environment usually happens around user stories with one
+or more developers working on it.
+
+If more than one developer then another branch for each developer is also used
+with his/her initials, and US id.
+
+After its tested merge into master and remove the branch.
+
+----------
+
+## What about GitLab
+Tools like GitLab enhance collaboration by adding dialog around code mainly
+through issues and merge requests.
diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md
new file mode 100755
index 00000000000..a60c4365e0c
--- /dev/null
+++ b/doc/university/training/topics/bisect.md
@@ -0,0 +1,81 @@
+# Bisect
+
+----------
+
+## Bisect
+
+- Find a commit that introduced a bug
+- Works through a process of elimination
+- Specify a known good and bad revision to begin
+
+----------
+
+## Bisect
+
+1. Start the bisect process
+2. Enter the bad revision (usually latest commit)
+3. Enter a known good revision (commit/branch)
+4. Run code to see if bug still exists
+5. Tell bisect the result
+6. Repeat the previous 2 items until you find the offending commit
+
+----------
+
+## Setup
+
+```
+ mkdir bisect-ex
+ cd bisect-ex
+ touch index.html
+ git add -A
+ git commit -m "starting out"
+ vi index.html
+ # Add all good
+ git add -A
+ git commit -m "second commit"
+ vi index.html
+ # Add all good 2
+ git add -A
+ git commit -m "third commit"
+ vi index.html
+```
+
+----------
+
+```
+ # Add all good 3
+ git add -A
+ git commit -m "fourth commit"
+ vi index.html
+ # This looks bad
+ git add -A
+ git commit -m "fifth commit"
+ vi index.html
+ # Really bad
+ git add -A
+ git commit -m "sixth commit"
+ vi index.html
+ # again just bad
+ git add -A
+ git commit -m "seventh commit"
+```
+
+----------
+
+## Commands
+
+```
+ git bisect start
+ # Test your code
+ git bisect bad
+ git bisect next
+ # Say yes to the warning
+ # Test
+ git bisect good
+ # Test
+ git bisect bad
+ # Test
+ git bisect good
+ # done
+ git bisect reset
+```
diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md
new file mode 100755
index 00000000000..af7a70a2818
--- /dev/null
+++ b/doc/university/training/topics/cherry_picking.md
@@ -0,0 +1,39 @@
+# Cherry Pick
+
+----------
+
+## Cherry Pick
+
+- Given an existing commit on one branch, apply the change to another branch
+- Useful for backporting bug fixes to previous release branches
+- Make the commit on the master branch and pick in to stable
+
+----------
+
+## Cherry Pick
+
+1. Check out a new 'stable' branch from 'master'
+1. Change back to 'master'
+1. Edit '`cherry_pick.rb`' and commit the changes.
+1. Check commit log to get the commit SHA
+1. Check out the 'stable' branch
+1. Cherry pick the commit using the SHA obtained earlier
+
+----------
+
+## Commands
+
+```bash
+git checkout master
+git checkout -b stable
+git checkout master
+
+# Edit `cherry_pick.rb`
+git add cherry_pick.rb
+git commit -m 'Fix bugs in cherry_pick.rb'
+git log
+# Copy commit SHA
+git checkout stable
+
+git cherry-pick <commit SHA>
+```
diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md
new file mode 100755
index 00000000000..8149379b36f
--- /dev/null
+++ b/doc/university/training/topics/env_setup.md
@@ -0,0 +1,60 @@
+# Configure your environment
+
+----------
+## Install
+
+- **Windows**
+ - Install 'Git for Windows' from https://git-for-windows.github.io
+
+- **Mac**
+ - Type '`git`' in the Terminal application.
+ - If it's not installed, it will prompt you to install it.
+
+- **Linux**
+ ```bash
+ sudo yum install git-all
+ ```
+ ```bash
+ sudo apt-get install git-all
+ ```
+
+----------
+
+## Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+----------
+
+## Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
diff --git a/doc/university/training/topics/explore_gitlab.md b/doc/university/training/topics/explore_gitlab.md
new file mode 100755
index 00000000000..b65457728c0
--- /dev/null
+++ b/doc/university/training/topics/explore_gitlab.md
@@ -0,0 +1,10 @@
+# Explore GitLab projects
+
+----------
+
+- Dashboard
+- User Preferences
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md
new file mode 100755
index 00000000000..4b34406ea75
--- /dev/null
+++ b/doc/university/training/topics/feature_branching.md
@@ -0,0 +1,32 @@
+# Feature branching
+
+----------
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+ - Hint: This is a cheap backup for your work-in-progress code
+
+----------
+
+## Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+----------
+
+## Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md
new file mode 100755
index 00000000000..ec7bb2631aa
--- /dev/null
+++ b/doc/university/training/topics/getting_started.md
@@ -0,0 +1,95 @@
+# Getting Started
+
+----------
+
+## Instantiating Repositories
+
+* Create a new repository by instantiating it through
+```bash
+git init
+```
+* Copy an existing project by cloning the repository through
+```bash
+git clone <url>
+```
+
+----------
+
+## Central Repos
+
+* To instantiate a central repository a `--bare` flag is required.
+* Bare repositories don't allow file editing or committing changes.
+* Create a bare repo with
+```bash
+git init --bare project-name.git
+```
+
+----------
+
+## Instantiate workflow with clone
+
+1. Create a project in your user namespace
+ - Choose to import from 'Any Repo by URL' and use
+ https://gitlab.com/gitlab-org/training-examples.git
+2. Create a '`Workspace`' directory in your home directory.
+3. Clone the '`training-examples`' project
+
+----------
+
+## Commands
+
+```
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+----------
+
+## Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+----------
+
+## Committing Workflow
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+----------
+
+## Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+----------
+
+## Note
+
+* git fetch vs pull
+* Pull is git fetch + git merge
diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md
new file mode 100755
index 00000000000..9ffb4b9c859
--- /dev/null
+++ b/doc/university/training/topics/git_add.md
@@ -0,0 +1,33 @@
+# Git Add
+
+----------
+
+## Git Add
+
+Adds content to the index or staging area.
+
+* Adds a list of file
+```bash
+git add <files>
+```
+* Adds all files including deleted ones
+```bash
+git add -A
+```
+
+----------
+
+## Git add continued
+
+* Add all text files in current dir
+```bash
+git add *.txt
+```
+* Add all text file in the project
+```bash
+git add "*.txt*"
+```
+* Adds all files in directory
+```bash
+git add views/layouts/
+```
diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md
new file mode 100755
index 00000000000..ca1ff29d93b
--- /dev/null
+++ b/doc/university/training/topics/git_intro.md
@@ -0,0 +1,24 @@
+# Git introduction
+
+----------
+
+## Intro
+
+https://git-scm.com/about
+
+- Distributed version control
+ - Does not rely on connection to a central server
+ - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+----------
+
+## Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
diff --git a/doc/university/training/topics/git_log.md b/doc/university/training/topics/git_log.md
new file mode 100755
index 00000000000..32ebceff491
--- /dev/null
+++ b/doc/university/training/topics/git_log.md
@@ -0,0 +1,57 @@
+# Git Log
+
+----------
+
+Git log lists commit history. It allows searching and filtering.
+
+* Initiate log
+```
+git log
+```
+
+* Retrieve set number of records:
+```
+git log -n 2
+```
+
+* Search commits by author. Allows user name or a regular expression.
+```
+git log --author="user_name"
+```
+
+----------
+
+* Search by comment message.
+```
+git log --grep="<pattern>"
+```
+
+* Search by date
+```
+git log --since=1.month.ago --until=3.weeks.ago
+```
+
+
+----------
+
+## Git Log Workflow
+
+1. Change to workspace directory
+2. Clone the multi runner projects
+3. Change to project dir
+4. Search by author
+5. Search by date
+6. Combine
+
+----------
+
+## Commands
+
+```
+cd ~/workspace
+git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
+cd gitlab-ci-multi-runner
+git log --author="Travis"
+git log --since=1.month.ago --until=3.weeks.ago
+git log --since=1.month.ago --until=1.day.ago --author="Travis"
+```
diff --git a/doc/university/training/topics/gitlab_flow.md b/doc/university/training/topics/gitlab_flow.md
new file mode 100755
index 00000000000..8e5d3baf959
--- /dev/null
+++ b/doc/university/training/topics/gitlab_flow.md
@@ -0,0 +1,53 @@
+# GitLab Flow
+
+----------
+
+- A simplified branching strategy
+- All features and fixes first go to master
+- Allows for 'production' or 'stable' branches
+- Bug fixes/hot fix patches are cherry-picked from master
+
+----------
+
+### Feature branches
+
+- Create a feature/bugfix branch to do all work
+- Use merge requests to merge to master
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
+
+----------
+
+## Production branch
+
+- One, long-running production release branch
+ as opposed to individual stable branches
+- Consider creating a tag for each version that gets deployed
+
+----------
+
+## Production branch
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
+
+----------
+
+## Release branch
+
+- Useful if you release software to customers
+- When preparing a new release, create stable branch
+ from master
+- Consider creating a tag for each version
+- Cherry-pick critical bug fixes to stable branch for patch release
+- Never commit bug fixes directly to stable branch
+
+----------
+
+![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
+
+----------
+
+## More details
+
+Blog post on 'GitLab Flow' at
+[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md
new file mode 100755
index 00000000000..77807b3e7ef
--- /dev/null
+++ b/doc/university/training/topics/merge_conflicts.md
@@ -0,0 +1,70 @@
+# Merge conflicts
+
+----------
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+----------
+
+## Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the `conflicts.rb` file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+----------
+
+## Commands
+
+```
+git checkout -b conflicts_branch
+
+# vi conflicts.rb
+# Add 'Line4' and 'Line5'
+
+git commit -am "add line4 and line5"
+git push origin conflicts_branch
+
+git checkout master
+
+# vi conflicts.rb
+# Add 'Line6' and 'Line7'
+git commit -am "add line6 and line7"
+git push origin master
+```
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+```
+git checkout conflicts_branch
+git fetch
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+# No need to commit this file
+
+git rebase --continue
+
+# Remember that we have rewritten our commit history so we
+# need to force push so that our remote branch is restructured
+git push origin conflicts_branch -f
+```
+----------
+
+## Note
+* When to use 'git merge' and when to use 'git rebase'
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md
new file mode 100755
index 00000000000..5b446f02f63
--- /dev/null
+++ b/doc/university/training/topics/merge_requests.md
@@ -0,0 +1,43 @@
+# Merge requests
+
+----------
+
+- When you want feedback create a merge request
+- Target is the default branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+----------
+
+## Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+----------
+
+## Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+----------
+
+## Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
diff --git a/doc/university/training/topics/rollback_commits.md b/doc/university/training/topics/rollback_commits.md
new file mode 100755
index 00000000000..cf647284604
--- /dev/null
+++ b/doc/university/training/topics/rollback_commits.md
@@ -0,0 +1,81 @@
+# Rollback Commits
+
+----------
+
+## Undo Commits
+
+* Undo last commit putting everything back into the staging area.
+```
+git reset --soft HEAD^
+```
+
+* Add files and change message with:
+```
+git commit --amend -m "New Message"
+```
+
+----------
+
+* Undo last and remove changes
+```
+git reset --hard HEAD^
+```
+
+* Same as last one but for two commits back
+```
+git reset --hard HEAD^^
+```
+
+** Don't reset after pushing **
+
+----------
+
+## Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+
+----------
+
+## Commands
+
+```
+# Change file edit_this_file.rb
+git status
+git commit -am "kjkfjkg"
+git log
+git commit --amend -m "New comment added"
+git log
+git reset --soft HEAD^
+git log
+git pull origin master
+git push origin master
+```
+
+----------
+
+## Note
+
+* git revert vs git reset
+* Reset removes the commit while revert removes the changes but leaves the commit
+* Revert is safer considering we can revert a revert
+
+```
+# Changed file
+git commit -am "bug introduced"
+git revert HEAD
+# New commit created reverting changes
+# Now we want to re apply the reverted commit
+git log # take hash from the revert commit
+git revert <rev commit hash>
+# reverted commit is back (new commit created again)
+```
diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md
new file mode 100755
index 00000000000..c1bdda32645
--- /dev/null
+++ b/doc/university/training/topics/stash.md
@@ -0,0 +1,86 @@
+# Git Stash
+
+----------
+
+We use git stash to store our changes when they are not ready to be committed
+and we need to change to a different branch.
+
+* Stash
+```
+git stash save
+# or
+git stash
+# or with a message
+git stash save "this is a message to display on the list"
+```
+
+* Apply stash to keep working on it
+```
+git stash apply
+# or apply a specific one from out stack
+git stash apply stash@{3}
+```
+
+----------
+
+* Every time we save a stash it gets stacked so by using list we can see all our
+stashes.
+
+```
+git stash list
+# or for more information (log methods)
+git stash list --stat
+```
+
+* To clean our stack we need to manually remove them.
+
+```
+# drop top stash
+git stash drop
+# or
+git stash drop <name>
+# to clear all history we can use
+git stash clear
+```
+
+----------
+
+* Apply and drop on one command
+
+```
+ git stash pop
+```
+
+* If we meet conflicts we need to either reset or commit our changes.
+
+* Conflicts through `pop` will not drop a stash afterwards.
+
+----------
+
+## Git Stash
+
+1. Modify a file
+2. Stage file
+3. Stash it
+4. View our stash list
+5. Confirm no pending changes through status
+5. Apply with pop
+6. View list to confirm changes
+
+----------
+
+## Commands
+
+```
+# Modify edit_this_file.rb file
+git add .
+
+git stash save "Saving changes from edit this file"
+
+git stash list
+git status
+
+git stash pop
+git stash list
+git status
+```
diff --git a/doc/university/training/topics/subtree.md b/doc/university/training/topics/subtree.md
new file mode 100755
index 00000000000..5d869af64c1
--- /dev/null
+++ b/doc/university/training/topics/subtree.md
@@ -0,0 +1,55 @@
+## Subtree
+
+----------
+
+## Subtree
+
+* Used when there are nested repositories.
+* Not recommended when the amount of dependencies is too large
+* For these cases we need a dependency control system
+* Command are painfully long so aliases are necessary
+
+----------
+
+## Subtree Aliases
+
+* Add: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Pull: git subtree add --prefix <target-folder> <url> <branch> --squash
+* Push: git subtree add --prefix <target-folder> <url> <branch>
+* Ex: git config alias.sbp 'subtree pull --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+
+----------
+
+```
+ # Add an alias
+ # Add
+ git config alias.sba 'subtree add --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+ # Pull
+ git config alias.sbpl 'subtree pull --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master --squash'
+ # Push
+ git config alias.sbph 'subtree push --prefix st /
+ git@gitlab.com:balameb/subtree-nested-example.git master'
+
+ # Adding this subtree adds a st dir with a readme
+ git sba
+ vi st/README.md
+ # Edit file
+ git status shows differences
+
+```
+
+----------
+
+```
+ # Adding, or committing won't change the sub repo at remote
+ # even if we push
+ git add -A
+ git commit -m "Adding to subtree readme"
+
+ # Push to subtree repo
+ git sbph
+ # now we can check our remote sub repo
+```
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
new file mode 100755
index 00000000000..e9607b5a875
--- /dev/null
+++ b/doc/university/training/topics/tags.md
@@ -0,0 +1,38 @@
+# Tags
+
+----------
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+----------
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+----------
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md
new file mode 100755
index 00000000000..17dbb64b9e6
--- /dev/null
+++ b/doc/university/training/topics/unstage.md
@@ -0,0 +1,31 @@
+# Unstage
+
+----------
+
+## Unstage
+
+* To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
+
+```bash
+git reset HEAD <file>
+```
+
+* This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+```bash
+git checkout -- <file>
+```
+
+----------
+
+* To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag.
+```
+git rm '*.txt'
+git rm -r <dirname>
+```
+
+
+* If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`.
+```
+git rm <filename> --cache
+```
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
new file mode 100755
index 00000000000..35afe73708f
--- /dev/null
+++ b/doc/university/training/user_training.md
@@ -0,0 +1,392 @@
+# GitLab Git Workshop
+
+---
+
+# Agenda
+
+1. Brief history of Git
+1. GitLab walkthrough
+1. Configure your environment
+1. Workshop
+
+---
+
+# Git introduction
+
+https://git-scm.com/about
+
+- Distributed version control
+ - Does not rely on connection to a central server
+ - Many copies of the complete history
+- Powerful branching and merging
+- Adapts to nearly any workflow
+- Fast, reliable and stable file format
+
+---
+
+# Help!
+
+Use the tools at your disposal when you get stuck.
+
+- Use '`git help <command>`' command
+- Use Google
+- Read documentation at https://git-scm.com
+
+---
+
+# GitLab Walkthrough
+
+![fit](logo.png)
+
+---
+
+# Configure your environment
+
+- Windows: Install 'Git for Windows'
+
+> https://git-for-windows.github.io
+
+- Mac: Type '`git`' in the Terminal application.
+
+> If it's not installed, it will prompt you to install it.
+
+- Debian: '`sudo apt-get install git-all`'
+or Red Hat '`sudo yum install git-all`'
+
+---
+
+# Git Workshop
+
+## Overview
+
+1. Configure Git
+1. Configure SSH Key
+1. Create a project
+1. Committing
+1. Feature branching
+1. Merge requests
+1. Feedback and Collaboration
+
+---
+
+# Configure Git
+
+One-time configuration of the Git client
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+---
+
+# Configure SSH Key
+
+```bash
+ssh-keygen -t rsa -b 4096 -C "you@computer-name"
+```
+
+```bash
+# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
+Generating public/private rsa key pair.
+Enter file in which to save the key (/Users/you/.ssh/id_rsa):
+Enter passphrase (empty for no passphrase):
+Enter same passphrase again:
+Your identification has been saved in /Users/you/.ssh/id_rsa.
+Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
+The key fingerprint is:
+39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
+```
+
+Copy your public key and add it to your GitLab profile
+
+```bash
+cat ~/.ssh/id_rsa.pub
+```
+
+```bash
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
+```
+
+---
+
+# Create a project
+
+- Create a project in your user namespace
+ - Choose to import from 'Any Repo by URL' and use
+ https://gitlab.com/gitlab-org/training-examples.git
+- Create a '`development`' or '`workspace`' directory in your home directory.
+- Clone the '`training-examples`' project
+
+---
+
+# Commands
+
+```
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace
+
+git clone git@gitlab.example.com:<username>/training-examples.git
+cd training-examples
+```
+
+---
+
+# Git concepts
+
+**Untracked files**
+
+New files that Git has not been told to track previously.
+
+**Working area**
+
+Files that have been modified but are not committed.
+
+**Staging area**
+
+Modified files that have been marked to go in the next commit.
+
+---
+
+# Committing
+
+1. Edit '`edit_this_file.rb`' in '`training-examples`'
+1. See it listed as a changed file (working area)
+1. View the differences
+1. Stage the file
+1. Commit
+1. Push the commit to the remote
+1. View the git log
+
+---
+
+# Commands
+
+```
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---
+
+# Feature branching
+
+- Efficient parallel workflow for teams
+- Develop each feature in a branch
+- Keeps changes isolated
+- Consider a 1-to-1 link to issues
+- Push branches to the server frequently
+ - Hint: This is a cheap backup for your work-in-progress code
+
+---
+
+# Feature branching
+
+1. Create a new feature branch called 'squash_some_bugs'
+1. Edit '`bugs.rb`' and remove all the bugs.
+1. Commit
+1. Push
+
+---
+
+# Commands
+
+```
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+# Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add 'WIP' to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+---
+
+# Merge requests
+
+**Create your first merge request**
+
+1. Use the blue button in the activity feed
+1. View the diff (changes) and leave a comment
+1. Push a new commit to the same branch
+1. Review the changes again and notice the update
+
+---
+
+# Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+
+---
+
+# Feedback and Collaboration
+
+Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
+[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
+
+See GitLab merge requests for examples:
+[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+# Explore GitLab projects
+
+![fit](logo.png)
+
+- Dashboard
+- User Preferences
+- ReadMe, Changelog, License shortcuts
+- Issues
+- Milestones and Labels
+- Manage project members
+- Project settings
+
+---
+
+# Tags
+
+- Useful for marking deployments and releases
+- Annotated tags are an unchangeable part of Git history
+- Soft/lightweight tags can be set and removed at will
+- Many projects combine an anotated release tag with a stable branch
+- Consider setting deployment/release tags automatically
+
+---
+
+# Tags
+
+- Create a lightweight tag
+- Create an annotated tag
+- Push the tags to the remote repository
+
+**Additional resources**
+
+[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+
+---
+
+# Commands
+
+```
+git checkout master
+
+# Lightweight tag
+git tag my_lightweight_tag
+
+# Annotated tag
+git tag -a v1.0 -m ‘Version 1.0’
+git tag
+
+git push origin --tags
+```
+
+---
+
+# Merge conflicts
+
+- Happen often
+- Learning to fix conflicts is hard
+- Practice makes perfect
+- Force push after fixing conflicts. Be careful!
+
+---
+
+# Merge conflicts
+
+1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
+1. Commit and push
+1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
+1. Commit and push to master
+1. Create a merge request
+
+---
+
+# Merge conflicts
+
+After creating a merge request you should notice that conflicts exist. Resolve
+the conflicts locally by rebasing.
+
+```
+git rebase master
+
+# Fix conflicts by editing the files.
+
+git add conflicts.rb
+git commit -m 'Fix conflicts'
+git rebase --continue
+git push origin <branch> -f
+```
+
+---
+
+# Rebase with squash
+
+You may end up with a commit log that looks like this:
+
+```
+Fix issue #13
+Test
+Fix
+Fix again
+Test
+Test again
+Does this work?
+```
+
+Squash these in to meaningful commits using an interactive rebase.
+
+---
+
+# Rebase with squash
+
+Squash the commits on the same branch we used for the merge conflicts step.
+
+```
+git rebase -i master
+```
+
+In the editor, leave the first commit as 'pick' and set others to 'fixup'.
+
+---
+
+# Questions?
+
+![fit](logo.png)
+
+Thank you for your hard work!
+
+**Additional Resources**
+
+GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
+GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
+Pro git book [http://git-scm.com/book](http://git-scm.com/book)
+Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
+Code School tutorial [http://try.github.io/](http://try.github.io/)
+Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7a7a0b864bd..162d1bd7ed4 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1,43 +1,5 @@
# Markdown
-## Table of Contents
-
-**[GitLab Flavored Markdown](#gitlab-flavored-markdown-gfm)**
-
-* [Newlines](#newlines)
-* [Multiple underscores in words](#multiple-underscores-in-words)
-* [URL auto-linking](#url-auto-linking)
-* [Multiline Blockquote](#multiline-blockquote)
-* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
-* [Inline Diff](#inline-diff)
-* [Emoji](#emoji)
-* [Special GitLab references](#special-gitlab-references)
-* [Task Lists](#task-lists)
-* [Videos](#videos)
-
-**[Standard Markdown](#standard-markdown)**
-
-* [Headers](#headers)
-* [Emphasis](#emphasis)
-* [Lists](#lists)
-* [Links](#links)
-* [Images](#images)
-* [Blockquotes](#blockquotes)
-* [Inline HTML](#inline-html)
-* [Horizontal Rule](#horizontal-rule)
-* [Line Breaks](#line-breaks)
-* [Tables](#tables)
-* [Footnotes](#footnotes)
-
-**[Wiki-Specific Markdown](#wiki-specific-markdown)**
-
-* [Wiki - Direct page link](#wiki-direct-page-link)
-* [Wiki - Direct file link](#wiki-direct-file-link)
-* [Wiki - Hierarchical link](#wiki-hierarchical-link)
-* [Wiki - Root link](#wiki-root-link)
-
-**[References](#references)**
-
## GitLab Flavored Markdown (GFM)
> **Note:**
@@ -64,7 +26,7 @@ You can use GFM in the following areas:
You can also use other rich text files in GitLab. You might have to install a
dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
-## Newlines
+### Newlines
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
@@ -84,7 +46,7 @@ Violets are blue
Sugar is sweet
-## Multiple underscores in words
+### Multiple underscores in words
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
@@ -99,7 +61,7 @@ perform_complicated_task
do_this_and_do_that_and_another_thing
-## URL auto-linking
+### URL auto-linking
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking
@@ -120,7 +82,7 @@ GFM will autolink almost any URL you copy and paste into your text:
* irc://irc.freenode.net/gitlab
* http://localhost:3000
-## Multiline Blockquote
+### Multiline Blockquote
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
@@ -154,7 +116,7 @@ multiple lines,
you can quote that without having to manually prepend `>` to every line!
>>>
-## Code and Syntax Highlighting
+### Code and Syntax Highlighting
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting
@@ -224,7 +186,7 @@ s = "There is no highlighting for this."
But let's throw in a <b>tag</b>.
```
-## Inline Diff
+### Inline Diff
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
@@ -240,7 +202,7 @@ However the wrapping tags cannot be mixed as such:
- {- deletions -]
- [- deletions -}
-## Emoji
+### Emoji
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
@@ -265,7 +227,7 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil
Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-## Special GitLab References
+### Special GitLab References
GFM recognizes special references.
@@ -305,7 +267,7 @@ GFM also recognizes certain cross-project references:
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
| `namespace/project~"Some label"` | issues with given label |
-## Task Lists
+### Task Lists
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists
@@ -328,7 +290,7 @@ You can add task lists to issues, merge requests and comments. To create a task
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
-## Videos
+### Videos
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
@@ -345,9 +307,9 @@ Here's a sample video:
![Sample Video](img/markdown_video.mp4)
-# Standard Markdown
+## Standard Markdown
-## Headers
+### Headers
```no-highlight
# H1
@@ -366,21 +328,6 @@ Alt-H2
------
```
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
-
-Alternatively, for H1 and H2, an underline-ish style:
-
-Alt-H1
-======
-
-Alt-H2
-------
-
### Header IDs and links
All Markdown-rendered headers automatically get IDs, except in comments.
@@ -416,7 +363,7 @@ Would generate the following link IDs:
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
-## Emphasis
+### Emphasis
```no-highlight
Emphasis, aka italics, with *asterisks* or _underscores_.
@@ -436,7 +383,7 @@ Combined emphasis with **asterisks and _underscores_**.
Strikethrough uses two tildes. ~~Scratch this.~~
-## Lists
+### Lists
```no-highlight
1. First ordered list item
@@ -492,7 +439,7 @@ the second list item will be incorrectly labeled as `1`.
Second paragraph of first item.
2. Another item
-## Links
+### Links
There are two ways to create links, inline-style and reference-style.
@@ -501,9 +448,9 @@ There are two ways to create links, inline-style and reference-style.
[I'm a reference-style link][Arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](LICENSE)
-
+
[I am an absolute reference within the repository](/doc/user/markdown.md)
-
+
[I link to the Milestones page](/../milestones)
[You can use numbers for reference-style link definitions][1]
@@ -523,9 +470,9 @@ There are two ways to create links, inline-style and reference-style.
[I'm a relative reference to a repository file](LICENSE)[^1]
[I am an absolute reference within the repository](/doc/user/markdown.md)
-
+
[I link to the Milestones page](/../milestones)
-
+
[You can use numbers for reference-style link definitions][1]
Or leave it empty and use the [link text itself][]
@@ -544,7 +491,8 @@ Relative links do not allow referencing project files in a wiki page or wiki pag
will point the link to `wikis/style` when the link is inside of a wiki markdown file.
-## Images
+
+### Images
Here's our logo (hover to see the title text):
@@ -568,7 +516,7 @@ Reference-style:
[logo]: img/markdown_logo.png
-## Blockquotes
+### Blockquotes
```no-highlight
> Blockquotes are very handy in email to emulate reply text.
@@ -586,11 +534,11 @@ Quote break.
> This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote.
-## Inline HTML
+### Inline HTML
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span` elements.
```no-highlight
<dl>
@@ -610,7 +558,7 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
-## Horizontal Rule
+### Horizontal Rule
```
Three or more...
@@ -642,7 +590,7 @@ ___
Underscores
-## Line Breaks
+### Line Breaks
My basic recommendation for learning how line breaks work is to experiment and discover -- hit &lt;Enter&gt; once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend.
@@ -672,7 +620,7 @@ This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
-## Tables
+### Tables
Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
@@ -708,16 +656,15 @@ By including colons in the header row, you can align the text within that column
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
-## Footnotes
-
-You can add footnotes to your text as follows.[^1]
-[^1]: This is my awesome footnote.
+### Footnotes
```
-You can add footnotes to your text as follows.[^1]
-[^1]: This is my awesome footnote.
+You can add footnotes to your text as follows.[^2]
+[^2]: This is my awesome footnote.
```
+You can add footnotes to your text as follows.[^2]
+
## Wiki-specific Markdown
The following examples show how links inside wikis behave.
@@ -752,30 +699,30 @@ A link can be constructed relative to the current wiki page using `./<page>`,
- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
it would link to `<your_wiki>/documentation/related`:
- ```markdown
- [Link to Related Page](./related)
- ```
+ ```markdown
+ [Link to Related Page](./related)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
it would link to `<your_wiki>/documentation/main`:
- ```markdown
- [Link to Related Page](../main)
- ```
+ ```markdown
+ [Link to Related Page](../main)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/main`,
it would link to `<your_wiki>/documentation/related.md`:
- ```markdown
- [Link to Related Page](./related.md)
- ```
+ ```markdown
+ [Link to Related Page](./related.md)
+ ```
- If this snippet was placed on a page at `<your_wiki>/documentation/related/content`,
it would link to `<your_wiki>/documentation/main.md`:
- ```markdown
- [Link to Related Page](../main.md)
- ```
+ ```markdown
+ [Link to Related Page](../main.md)
+ ```
### Wiki - Root link
@@ -783,22 +730,25 @@ A link starting with a `/` is relative to the wiki root.
- This snippet links to `<wiki_root>/documentation`:
- ```markdown
- [Link to Related Page](/documentation)
- ```
+ ```markdown
+ [Link to Related Page](/documentation)
+ ```
- This snippet links to `<wiki_root>/miscellaneous.md`:
- ```markdown
- [Link to Related Page](/miscellaneous.md)
- ```
+ ```markdown
+ [Link to Related Page](/miscellaneous.md)
+ ```
+
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
+[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
+[^2]: This is my awesome footnote.
+
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
-[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index d6216a8dd50..cea78864df2 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -32,6 +32,8 @@ The following table depicts the various user permission levels in a project.
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
+| Create new environments | | | ✓ | ✓ | ✓ |
+| Stop environments | | | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
@@ -45,7 +47,6 @@ The following table depicts the various user permission levels in a project.
| Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
-| Create new environments | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
@@ -58,7 +59,6 @@ The following table depicts the various user permission levels in a project.
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
-| Delete environments | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2215f37b81a..c228ea72f22 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -279,7 +279,7 @@ The trick is to use the merge/pull request with multiple commits when your work
The commit message should reflect your intention, not the contents of the commit.
The contents of the commit can be easily seen anyway, the question is why you did it.
An example of a good commit message is: "Combine templates to dry up the user views.".
-Some words that are bad commit messages because they don't contain munch information are: change, improve and refactor.
+Some words that are bad commit messages because they don't contain much information are: change, improve and refactor.
The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number.
To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 1b49a5c385f..c936e8833c6 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -66,6 +66,7 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
+ - the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
@@ -88,6 +89,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | |
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+| Failed pipeline | The above, plus the author of the pipeline |
+| Successful pipeline | The above, plus the author of the pipeline |
In addition, if the title or description of an Issue or Merge Request is
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index a50ba305deb..54e7ae19ea5 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -24,9 +24,11 @@ you still have open.
A Todo appears in your Todos dashboard when:
-- an issue or merge request is assigned to you
+- an issue or merge request is assigned to you,
- you are `@mentioned` in an issue or merge request, be it the description of
- the issue/merge request or in a comment
+ the issue/merge request or in a comment,
+- build in the CI pipeline running for your merge request failed, but this
+ build is not allowed to fail.
>**Note:** Commenting on a commit will _not_ trigger a Todo.
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 447dd92a458..dc1339deb4c 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -59,11 +59,6 @@ Feature: Profile
When I unsuccessfully change my password
Then I should see a password error message
- Scenario: I reset my token
- Given I visit profile account page
- Then I reset my token
- And I should see new token
-
Scenario: I visit history tab
Given I have activity
When I visit Audit Log page
diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature
index 89a02706bd2..93c884e23c5 100644
--- a/features/project/network_graph.feature
+++ b/features/project/network_graph.feature
@@ -43,4 +43,4 @@ Feature: Project Network Graph
Scenario: I should fail to look for a commit
When I look for a commit by ";"
- Then page status code should be 404
+ Then I should see non-existent git revision error message
diff --git a/features/project/source/git_blame.feature b/features/project/source/git_blame.feature
deleted file mode 100644
index 48b1077dc6b..00000000000
--- a/features/project/source/git_blame.feature
+++ /dev/null
@@ -1,10 +0,0 @@
-Feature: Project Source Git Blame
- Background:
- Given I sign in as a user
- And I own project "Shop"
- Given I visit project source page
-
- Scenario: I blame file
- Given I click on ".gitignore" file in repo
- And I click Blame button
- Then I should see git file blame
diff --git a/features/snippet_search.feature b/features/snippet_search.feature
deleted file mode 100644
index 834bd3b2376..00000000000
--- a/features/snippet_search.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@dashboard
-Feature: Snippet Search
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have a public many lined snippet
-
- Scenario: I should see my public and private snippets
- When I search for "snippet" in snippet titles
- Then I should see "Personal snippet one" in results
- And I should see "Personal snippet private" in results
-
- Scenario: I should see three surrounding lines on either side of a matching snippet line
- When I search for "line seven" in snippet contents
- Then I should see "line four" in results
- And I should see "line seven" in results
- And I should see "line ten" in results
- And I should not see "line three" in results
- And I should not see "line eleven" in results
diff --git a/features/snippets/discover.feature b/features/snippets/discover.feature
deleted file mode 100644
index 1a7e132ea25..00000000000
--- a/features/snippets/discover.feature
+++ /dev/null
@@ -1,13 +0,0 @@
-@snippets
-Feature: Snippets Discover
- Background:
- Given I sign in as a user
- And I have public "Personal snippet one" snippet
- And I have private "Personal snippet private" snippet
- And I have internal "Personal snippet internal" snippet
-
- Scenario: I should see snippets
- Given I visit snippets page
- Then I should see "Personal snippet one" in snippets
- And I should see "Personal snippet internal" in snippets
- And I should not see "Personal snippet private" in snippets
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 0e81e99120b..0c88838767c 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -117,7 +117,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'I visit group "NonExistentGroup" page' do
- visit group_path(-1)
+ visit group_path("NonExistentGroup")
end
step 'the archived project have some issues' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 05ab2a7dc73..ea480d2ad68 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -104,18 +104,6 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
end
- step 'I reset my token' do
- page.within '.private-token' do
- @old_token = @user.private_token
- click_button "Reset private token"
- end
- end
-
- step 'I should see new token' do
- expect(find("#token").value).not_to eq @old_token
- expect(find("#token").value).to eq @user.reload.private_token
- end
-
step 'I have activity' do
create(:closed_issue_event, author: current_user)
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 2ccab4334eb..f728d243cdc 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -413,37 +413,37 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Hide inline discussion" of the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I click link "Show inline discussion" of the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
find('.js-toggle-diff-comments').trigger('click')
end
end
step 'I should not see a comment like "Line is wrong" in the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong"
end
end
step 'I should see a comment like "Line is wrong" in the third file' do
- page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
+ page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
end
end
step 'I should not see a comment like "Line is wrong here" in the third file' do
- page.within '.files [id^=diff]:nth-child(3)' do
+ page.within '.files>div:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong here"
end
end
step 'I should see a comment like "Line is wrong here" in the third file' do
- page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
+ page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong here"
end
end
@@ -456,7 +456,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Comment"
end
- page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
+ page.within ".files>div:nth-child(2) .note-body > .note-text" do
expect(page).to have_content "Line is correct"
end
end
@@ -471,7 +471,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should still see a comment like "Line is correct" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ page.within '.files>div:nth-child(2) .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -494,7 +494,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see comments on the side-by-side diff page' do
- page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do
+ page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 019b3124a86..ff9251615c9 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -109,4 +109,8 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
find('button').click
end
end
+
+ step 'I should see non-existent git revision error message' do
+ expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist."
+ end
end
diff --git a/features/steps/project/source/git_blame.rb b/features/steps/project/source/git_blame.rb
deleted file mode 100644
index d0a27f47e2a..00000000000
--- a/features/steps/project/source/git_blame.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Spinach::Features::ProjectSourceGitBlame < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I click on ".gitignore" file in repo' do
- click_link ".gitignore"
- end
-
- step 'I click Blame button' do
- click_link 'Blame'
- end
-
- step 'I should see git file blame' do
- expect(page).to have_content "*.rb"
- expect(page).to have_content "Dmitriy Zaporozhets"
- expect(page).to have_content "Initial commit"
- end
-end
diff --git a/features/steps/shared/search.rb b/features/steps/shared/search.rb
deleted file mode 100644
index 6c3d601763d..00000000000
--- a/features/steps/shared/search.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module SharedSearch
- include Spinach::DSL
-
- def search_snippet_contents(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_blobs"
- end
-
- def search_snippet_titles(query)
- visit "/search?search=#{URI::encode(query)}&snippets=true&scope=snippet_titles"
- end
-end
diff --git a/features/steps/snippet_search.rb b/features/steps/snippet_search.rb
deleted file mode 100644
index 32e29ffad1e..00000000000
--- a/features/steps/snippet_search.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::SnippetSearch < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
- include SharedUser
- include SharedSearch
-
- step 'I search for "snippet" in snippet titles' do
- search_snippet_titles 'snippet'
- end
-
- step 'I search for "snippet private" in snippet titles' do
- search_snippet_titles 'snippet private'
- end
-
- step 'I search for "line seven" in snippet contents' do
- search_snippet_contents 'line seven'
- end
-
- step 'I should see "line seven" in results' do
- expect(page).to have_content 'line seven'
- end
-
- step 'I should see "line four" in results' do
- expect(page).to have_content 'line four'
- end
-
- step 'I should see "line ten" in results' do
- expect(page).to have_content 'line ten'
- end
-
- step 'I should not see "line eleven" in results' do
- expect(page).not_to have_content 'line eleven'
- end
-
- step 'I should not see "line three" in results' do
- expect(page).not_to have_content 'line three'
- end
-
- step 'I should see "Personal snippet one" in results' do
- expect(page).to have_content 'Personal snippet one'
- end
-
- step 'I should see "Personal snippet private" in results' do
- expect(page).to have_content 'Personal snippet private'
- end
-
- step 'I should not see "Personal snippet one" in results' do
- expect(page).not_to have_content 'Personal snippet one'
- end
-
- step 'I should not see "Personal snippet private" in results' do
- expect(page).not_to have_content 'Personal snippet private'
- end
-end
diff --git a/features/steps/snippets/discover.rb b/features/steps/snippets/discover.rb
deleted file mode 100644
index 76379d09d02..00000000000
--- a/features/steps/snippets/discover.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class Spinach::Features::SnippetsDiscover < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
-
- step 'I should see "Personal snippet one" in snippets' do
- expect(page).to have_content "Personal snippet one"
- end
-
- step 'I should see "Personal snippet internal" in snippets' do
- expect(page).to have_content "Personal snippet internal"
- end
-
- step 'I should not see "Personal snippet private" in snippets' do
- expect(page).not_to have_content "Personal snippet private"
- end
-
- def snippet
- @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
- end
-end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 87915b19480..ed723b94cfd 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -48,7 +48,7 @@ module API
put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
status :created
present member.user, with: Entities::Member, member: member
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 21a106387f0..73aed624ea7 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -128,6 +128,18 @@ module API
render_api_error!(result[:message], result[:return_code])
end
end
+
+ # Delete all merged branches
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id/repository/branches/delete_merged
+ delete ":id/repository/merged_branches" do
+ DeleteMergedBranchesService.new(user_project, current_user).async_execute
+
+ status(200)
+ end
end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index fb2a4148011..b6281a7f0ac 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -36,8 +36,7 @@ module API
optional :font, type: String, desc: 'Foreground color'
end
post do
- create_params = declared(params, include_missing: false).to_h
- message = BroadcastMessage.create(create_params)
+ message = BroadcastMessage.create(declared_params(include_missing: false))
if message.persisted?
present message, with: Entities::BroadcastMessage
@@ -73,9 +72,8 @@ module API
end
put ':id' do
message = find_message
- update_params = declared(params, include_missing: false).to_h
- if message.update(update_params)
+ if message.update(declared_params(include_missing: false))
present message, with: Entities::BroadcastMessage
else
render_validation_error!(message)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 2f2cf769481..f412e1da1bf 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -53,7 +53,7 @@ module API
post ":id/repository/commits" do
authorize! :push_code, user_project
- attrs = declared(params)
+ attrs = declared_params
attrs[:source_branch] = attrs[:branch_name]
attrs[:target_branch] = attrs[:branch_name]
attrs[:actions].map! do |action|
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 425df2c176a..85360730841 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -82,7 +82,7 @@ module API
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared(params)).execute
+ current_user, declared_params).execute
if key
present key, with: Entities::SSHKey
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9dd36ec969e..33cb6fd3704 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -159,7 +159,7 @@ module API
end
class RepoTreeObject < Grape::Entity
- expose :id, :name, :type
+ expose :id, :name, :type, :path
expose :mode do |obj, options|
filemode = obj.mode.to_s(8)
@@ -218,7 +218,7 @@ module API
expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options|
- issue.subscribed?(options[:current_user])
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
expose :user_notes_count
expose :upvotes, :downvotes
@@ -248,7 +248,7 @@ module API
expose :diff_head_sha, as: :sha
expose :merge_commit_sha
expose :subscribed do |merge_request, options|
- merge_request.subscribed?(options[:current_user])
+ merge_request.subscribed?(options[:current_user], options[:project])
end
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
@@ -433,17 +433,28 @@ module API
end
class LabelBasic < Grape::Entity
- expose :name, :color, :description
+ expose :id, :name, :color, :description
end
class Label < LabelBasic
- expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
+ expose :open_issues_count do |label, options|
+ label.open_issues_count(options[:current_user])
+ end
+
+ expose :closed_issues_count do |label, options|
+ label.closed_issues_count(options[:current_user])
+ end
+
+ expose :open_merge_requests_count do |label, options|
+ label.open_merge_requests_count(options[:current_user])
+ end
+
expose :priority do |label, options|
label.priority(options[:project])
end
expose :subscribed do |label, options|
- label.subscribed?(options[:current_user])
+ label.subscribed?(options[:current_user], options[:project])
end
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 819f80d8365..00c901937b1 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -32,8 +32,7 @@ module API
post ':id/environments' do
authorize! :create_environment, user_project
- create_params = declared(params, include_parent_namespaces: false).to_h
- environment = user_project.environments.create(create_params)
+ environment = user_project.environments.create(declared_params)
if environment.persisted?
present environment, with: Entities::Environment
@@ -55,8 +54,8 @@ module API
authorize! :update_environment, user_project
environment = user_project.environments.find(params[:environment_id])
-
- update_params = declared(params, include_missing: false).extract!(:name, :external_url).to_h
+
+ update_params = declared_params(include_missing: false).extract!(:name, :external_url)
if environment.update(update_params)
present environment, with: Entities::Environment
else
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index a13e353b7f5..48ad3b80ae0 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,108 +1,115 @@
module API
- # groups API
class Groups < Grape::API
before { authenticate! }
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the group'
+ optional :visibility_level, type: Integer, desc: 'The visibility level of the group'
+ optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ end
+ end
+
resource :groups do
- # Get a groups list
- #
- # Parameters:
- # skip_groups (optional) - Array of group ids to exclude from list
- # all_available (optional, boolean) - Show all group that you have access to
- # Example Request:
- # GET /groups
+ desc 'Get a groups list' do
+ success Entities::Group
+ end
+ params do
+ optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+ optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
+ optional :search, type: String, desc: 'Search for a specific group'
+ optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
+ end
get do
- @groups = if current_user.admin
- Group.all
- elsif params[:all_available]
- GroupsFinder.new.execute(current_user)
- else
- current_user.groups
- end
+ groups = if current_user.admin
+ Group.all
+ elsif params[:all_available]
+ GroupsFinder.new.execute(current_user)
+ else
+ current_user.groups
+ end
+
+ groups = groups.search(params[:search]) if params[:search].present?
+ groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
+ groups = groups.reorder(params[:order_by] => params[:sort].to_sym)
- @groups = @groups.search(params[:search]) if params[:search].present?
- @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
- @groups = paginate @groups
- present @groups, with: Entities::Group
+ present paginate(groups), with: Entities::Group
end
- # Create group. Available only for users who can create groups.
- #
- # Parameters:
- # name (required) - The name of the group
- # path (required) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # POST /groups
+ desc 'Get list of owned groups for authenticated user' do
+ success Entities::Group
+ end
+ get '/owned' do
+ groups = current_user.owned_groups
+ present paginate(groups), with: Entities::Group, user: current_user
+ end
+
+ desc 'Create a group. Available only for users who can create groups.' do
+ success Entities::Group
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the group'
+ requires :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ end
post do
authorize! :create_group
- required_attributes! [:name, :path]
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
- @group = Group.new(attrs)
+ group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if @group.save
- @group.add_owner(current_user)
- present @group, with: Entities::Group
+ if group.persisted?
+ present group, with: Entities::Group
else
- render_api_error!("Failed to save group #{@group.errors.messages}", 400)
+ render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
end
+ end
- # Update group. Available only for users who can administrate groups.
- #
- # Parameters:
- # id (required) - The ID of a group
- # path (optional) - The path of the group
- # description (optional) - The description of the group
- # visibility_level (optional) - The visibility level of the group
- # lfs_enabled (optional) - Enable/disable LFS for the projects in this group
- # request_access_enabled (optional) - Allow users to request member access
- # Example Request:
- # PUT /groups/:id
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Update a group. Available only for users who can administrate groups.' do
+ success Entities::Group
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the group'
+ optional :path, type: String, desc: 'The path of the group'
+ use :optional_params
+ at_least_one_of :name, :path, :description, :visibility_level,
+ :lfs_enabled, :request_access_enabled
+ end
put ':id' do
group = find_group(params[:id])
authorize! :admin_group, group
- attrs = attributes_for_keys [:name, :path, :description, :visibility_level, :lfs_enabled, :request_access_enabled]
-
- if ::Groups::UpdateService.new(group, current_user, attrs).execute
+ if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
present group, with: Entities::GroupDetail
else
render_validation_error!(group)
end
end
- # Get a single group, with containing projects
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # GET /groups/:id
+ desc 'Get a single group, with containing projects.' do
+ success Entities::GroupDetail
+ end
get ":id" do
group = find_group(params[:id])
present group, with: Entities::GroupDetail
end
- # Remove group
- #
- # Parameters:
- # id (required) - The ID of a group
- # Example Request:
- # DELETE /groups/:id
+ desc 'Remove a group.'
delete ":id" do
group = find_group(params[:id])
authorize! :admin_group, group
DestroyGroupService.new(group, current_user).execute
end
- # Get a list of projects in this group
- #
- # Example Request:
- # GET /groups/:id/projects
+ desc 'Get a list of projects in this group.' do
+ success Entities::Project
+ end
get ":id/projects" do
group = find_group(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
@@ -110,13 +117,12 @@ module API
present projects, with: Entities::Project, user: current_user
end
- # Transfer a project to the Group namespace
- #
- # Parameters:
- # id - group id
- # project_id - project id
- # Example Request:
- # POST /groups/:id/projects/:project_id
+ desc 'Transfer a project to the group namespace. Available only for admin.' do
+ success Entities::GroupDetail
+ end
+ params do
+ requires :project_id, type: String, desc: 'The ID of the project'
+ end
post ":id/projects/:project_id" do
authenticated_as_admin!
group = Group.find_by(id: params[:id])
@@ -124,7 +130,7 @@ module API
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
- present group
+ present group, with: Entities::GroupDetail
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 3c9d7b1aaef..2c593dbb4ea 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -23,6 +23,11 @@ module API
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
+ def declared_params(options = {})
+ options = { include_parent_namespaces: false }.merge(options)
+ declared(params, options).to_h.symbolize_keys
+ end
+
def find_user_by_private_token
token = private_token
return nil unless token.present?
@@ -80,26 +85,11 @@ module API
end
end
- def project_service
- @project_service ||= begin
- underscored_service = params[:service_slug].underscore
-
- if Service.available_services_names.include?(underscored_service)
- user_project.build_missing_services
-
- service_method = "#{underscored_service}_service"
-
- send_service(service_method)
- end
- end
-
+ def project_service(project = user_project)
+ @project_service ||= project.find_or_initialize_service(params[:service_slug].underscore)
@project_service || not_found!("Service")
end
- def send_service(service_method)
- user_project.send(service_method)
- end
-
def service_attributes
@service_attributes ||= project_service.fields.inject([]) do |arr, hash|
arr << hash[:name].to_sym
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
new file mode 100644
index 00000000000..eb223c1101d
--- /dev/null
+++ b/lib/api/helpers/internal_helpers.rb
@@ -0,0 +1,57 @@
+module API
+ module Helpers
+ module InternalHelpers
+ # Project paths may be any of the following:
+ # * /repository/storage/path/namespace/project
+ # * /namespace/project
+ # * namespace/project
+ #
+ # In addition, they may have a '.git' extension and multiple namespaces
+ #
+ # Transform all these cases to 'namespace/project'
+ def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ project_path = project_path.sub(/\.git\z/, '')
+
+ storage_paths.each do |storage_path|
+ storage_path = File.expand_path(storage_path)
+
+ if project_path.start_with?(storage_path)
+ project_path = project_path.sub(storage_path, '')
+ break
+ end
+ end
+
+ project_path.sub(/\A\//, '')
+ end
+
+ def project_path
+ @project_path ||= clean_project_path(params[:project])
+ end
+
+ def wiki?
+ @wiki ||= project_path.end_with?('.wiki') &&
+ !Project.find_with_namespace(project_path)
+ end
+
+ def project
+ @project ||= begin
+ # Check for *.wiki repositories.
+ # Strip out the .wiki from the pathname before finding the
+ # project. This applies the correct project permissions to
+ # the wiki repository as well.
+ project_path.chomp!('.wiki') if wiki?
+
+ Project.find_with_namespace(project_path)
+ end
+ end
+
+ def ssh_authentication_abilities
+ [
+ :read_project,
+ :download_code,
+ :push_code
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index ccf181402f9..7087ce11401 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -3,6 +3,8 @@ module API
class Internal < Grape::API
before { authenticate_by_gitlab_shell_token! }
+ helpers ::API::Helpers::InternalHelpers
+
namespace 'internal' do
# Check if git command is allowed to project
#
@@ -14,42 +16,6 @@ module API
# ref - branch name
# forced_push - forced_push
# protocol - Git access protocol being used, e.g. HTTP or SSH
- #
-
- helpers do
- def project_path
- @project_path ||= begin
- project_path = params[:project].sub(/\.git\z/, '')
- Repository.remove_storage_from_path(project_path)
- end
- end
-
- def wiki?
- @wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
- end
-
- def project
- @project ||= begin
- # Check for *.wiki repositories.
- # Strip out the .wiki from the pathname before finding the
- # project. This applies the correct project permissions to
- # the wiki repository as well.
- project_path.chomp!('.wiki') if wiki?
-
- Project.find_with_namespace(project_path)
- end
- end
-
- def ssh_authentication_abilities
- [
- :read_project,
- :download_code,
- :push_code
- ]
- end
- end
-
post "/allowed" do
status 200
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c9689e6f8ef..eea5b91d4f9 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -120,7 +120,7 @@ module API
issues = issues.reorder(issuable_order_by => issuable_sort)
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
# Get a single project issue
@@ -132,7 +132,7 @@ module API
# GET /projects/:id/issues/:issue_id
get ":id/issues/:issue_id" do
@issue = find_project_issue(params[:issue_id])
- present @issue, with: Entities::Issue, current_user: current_user
+ present @issue, with: Entities::Issue, current_user: current_user, project: user_project
end
# Create a new project issue
@@ -174,7 +174,7 @@ module API
end
if issue.valid?
- present issue, with: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
@@ -217,7 +217,7 @@ module API
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
if issue.valid?
- present issue, with: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
else
render_validation_error!(issue)
end
@@ -239,7 +239,7 @@ module API
begin
issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
- present issue, with: Entities::Issue, current_user: current_user
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
rescue ::Issues::MoveService::MoveError => error
render_api_error!(error.message, 400)
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 97218054f37..652786d4e3e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -30,10 +30,7 @@ module API
conflict!('Label already exists') if label
priority = params.delete(:priority)
- label_params = declared(params,
- include_parent_namespaces: false,
- include_missing: false).to_h
- label = user_project.labels.create(label_params)
+ label = user_project.labels.create(declared_params(include_missing: false))
if label.valid?
label.prioritize!(user_project, priority) if priority
@@ -77,11 +74,9 @@ module API
update_priority = params.key?(:priority)
priority = params.delete(:priority)
- label_params = declared(params,
- include_parent_namespaces: false,
- include_missing: false).to_h
+ label_params = declared_params(include_missing: false)
# Rename new name to the actual label attribute name
- label_params[:name] = label_params.delete('new_name') if label_params.key?('new_name')
+ label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name)
render_validation_error!(label) unless label.update(label_params)
diff --git a/lib/api/members.rb b/lib/api/members.rb
index b80818f0eb6..2d4d5cedf20 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -120,7 +120,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(source, current_user, declared(params)).execute
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
present member.user, with: Entities::Member, member: member
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index bf8504e1101..4176c7eec06 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -1,8 +1,12 @@
module API
- # MergeRequest API
class MergeRequests < Grape::API
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
+
before { authenticate! }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
helpers do
def handle_merge_request_errors!(errors)
@@ -18,27 +22,27 @@ module API
render_api_error!(errors, 400)
end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the merge request'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ end
end
- # List merge requests
- #
- # Parameters:
- # id (required) - The ID of a project
- # iid (optional) - Return the project MR having the given `iid`
- # state (optional) - Return requests "merged", "opened" or "closed"
- # order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- # sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- #
- # Example:
- # GET /projects/:id/merge_requests
- # GET /projects/:id/merge_requests?state=opened
- # GET /projects/:id/merge_requests?state=closed
- # GET /projects/:id/merge_requests?order_by=created_at
- # GET /projects/:id/merge_requests?order_by=updated_at
- # GET /projects/:id/merge_requests?sort=desc
- # GET /projects/:id/merge_requests?sort=asc
- # GET /projects/:id/merge_requests?iid=42
- #
+ desc 'List merge requests' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed merged all], default: 'all',
+ desc: 'Return opened, closed, merged, or all merge requests'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return merge requests sorted in `asc` or `desc` order.'
+ optional :iid, type: Integer, desc: 'The IID of the merge requests'
+ end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
@@ -48,60 +52,51 @@ module API
end
merge_requests =
- case params["state"]
- when "opened" then merge_requests.opened
- when "closed" then merge_requests.closed
- when "merged" then merge_requests.merged
+ case params[:state]
+ when 'opened' then merge_requests.opened
+ when 'closed' then merge_requests.closed
+ when 'merged' then merge_requests.merged
else merge_requests
end
merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
- present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user
+ present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Create MR
- #
- # Parameters:
- #
- # id (required) - The ID of a project - this will be the source of the merge request
- # source_branch (required) - The source branch
- # target_branch (required) - The target branch
- # target_project_id - The target project of the merge request defaults to the :id of the project
- # assignee_id - Assignee user ID
- # title (required) - Title of MR
- # description - Description of MR
- # labels (optional) - Labels for MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- #
- # Example:
- # POST /projects/:id/merge_requests
- #
+ desc 'Create a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the merge request'
+ requires :source_branch, type: String, desc: 'The source branch'
+ requires :target_branch, type: String, desc: 'The target branch'
+ optional :target_project_id, type: Integer,
+ desc: 'The target project of the merge request defaults to the :id of the project'
+ use :optional_params
+ end
post ":id/merge_requests" do
authorize! :create_merge_request, user_project
- required_attributes! [:source_branch, :target_branch, :title]
- attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id, :description, :milestone_id]
+
+ mr_params = declared_params
# Validate label names in advance
- if (errors = validate_label_params(params)).any?
+ if (errors = validate_label_params(mr_params)).any?
render_api_error!({ labels: errors }, 400)
end
- attrs[:labels] = params[:labels] if params[:labels]
-
- merge_request = ::MergeRequests::CreateService.new(user_project, current_user, attrs).execute
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
- # Delete a MR
- #
- # Parameters:
- # id (required) - The ID of the project
- # merge_request_id (required) - The MR id
+ desc 'Delete a merge request'
+ params do
+ 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])
@@ -112,109 +107,83 @@ module API
# Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "merge_requests/:merge_request_id/..." instead.
#
- [":id/merge_request/:merge_request_id", ":id/merge_requests/:merge_request_id"].each do |path|
- # Show MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id
- #
+ params do
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+ { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status|
+ desc 'Get a single merge request' do
+ if status == :deprecated
+ detail DEPRECATION_MESSAGE
+ end
+ success Entities::MergeRequest
+ end
get path do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :read_merge_request, merge_request
-
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Show MR commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/commits
- #
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
get "#{path}/commits" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.commits, with: Entities::RepoCommit
end
- # Show MR changes
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - The ID of MR
- #
- # Example:
- # GET /projects/:id/merge_requests/:merge_request_id/changes
- #
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
get "#{path}/changes" do
- merge_request = user_project.merge_requests.
- find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
- # Update MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # target_branch - The target branch
- # assignee_id - Assignee user ID
- # title - Title of MR
- # state_event - Status of MR. (close|reopen|merge)
- # description - Description of MR
- # labels (optional) - Labels for a MR as a comma-separated list
- # milestone_id (optional) - Milestone ID
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id
- #
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, desc: 'The title of the merge request'
+ optional :target_branch, type: String, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen merge],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event
+ end
put path do
- attrs = attributes_for_keys [:target_branch, :assignee_id, :title, :state_event, :description, :milestone_id]
- merge_request = user_project.merge_requests.find(params[:merge_request_id])
+ merge_request = user_project.merge_requests.find(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
- # Ensure source_branch is not specified
- if params[:source_branch].present?
- render_api_error!('Source branch cannot be changed', 400)
- end
+ mr_params = declared_params(include_missing: false)
# Validate label names in advance
- if (errors = validate_label_params(params)).any?
+ if (errors = validate_label_params(mr_params)).any?
render_api_error!({ labels: errors }, 400)
end
- attrs[:labels] = params[:labels] if params[:labels]
-
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, attrs).execute(merge_request)
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
if merge_request.valid?
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
else
handle_merge_request_errors! merge_request.errors
end
end
- # Merge MR
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # merge_commit_message (optional) - Custom merge commit message
- # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
- # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
- # sha (optional) - When present, must have the HEAD SHA of the source branch
- # Example:
- # PUT /projects/:id/merge_requests/:merge_request_id/merge
- #
+ desc 'Merge a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
+ optional :should_remove_source_branch, type: Boolean,
+ desc: 'When true, the source branch will be deleted if possible'
+ optional :merge_when_build_succeeds, type: Boolean,
+ desc: 'When true, this merge request will be merged when the build succeeds'
+ 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])
@@ -235,7 +204,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if to_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active?
+ if params[:merge_when_build_succeeds] && merge_request.pipeline && merge_request.pipeline.active?
::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
execute(merge_request)
else
@@ -243,14 +212,12 @@ module API
execute(merge_request)
end
- present merge_request, with: Entities::MergeRequest, current_user: current_user
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
- # Cancel Merge if Merge When build succeeds is enabled
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- #
+ desc 'Cancel merge if "Merge when build succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
post "#{path}/cancel_merge_when_build_succeeds" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -259,17 +226,10 @@ module API
::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use GET "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Get a merge request's comments
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
get "#{path}/comments" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
@@ -278,23 +238,15 @@ module API
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
- # Duplicate. DEPRECATED and WILL BE REMOVED in 9.0.
- # Use POST "/projects/:id/merge_requests/:merge_request_id/notes" instead
- #
- # Post comment to merge request
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # note (required) - Text of comment
- # Examples:
- # POST /projects/:id/merge_requests/:merge_request_id/comments
- #
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
post "#{path}/comments" do
- required_attributes! [:note]
-
merge_request = user_project.merge_requests.find(params[:merge_request_id])
-
authorize! :create_note, merge_request
opts = {
@@ -312,13 +264,9 @@ module API
end
end
- # List issues that will close on merge
- #
- # Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # Examples:
- # GET /projects/:id/merge_requests/:merge_request_id/closes_issues
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 9b73f6826cf..937c118779d 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -11,19 +11,25 @@ module API
else milestones
end
end
+
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the milestone'
+ optional :due_date, type: String, desc: 'The due date of the milestone'
+ end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a list of project milestones
- #
- # Parameters:
- # id (required) - The ID of a project
- # state (optional) - Return "active" or "closed" milestones
- # Example Request:
- # GET /projects/:id/milestones
- # GET /projects/:id/milestones?iid=42
- # GET /projects/:id/milestones?state=active
- # GET /projects/:id/milestones?state=closed
+ desc 'Get a list of project milestones' do
+ success Entities::Milestone
+ end
+ params do
+ optional :state, type: String, values: %w[active closed all], default: 'all',
+ desc: 'Return "active", "closed", or "all" milestones'
+ optional :iid, type: Integer, desc: 'The IID of the milestone'
+ end
get ":id/milestones" do
authorize! :read_milestone, user_project
@@ -34,34 +40,30 @@ module API
present paginate(milestones), with: Entities::Milestone
end
- # Get a single project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # Example Request:
- # GET /projects/:id/milestones/:milestone_id
+ desc 'Get a single project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ end
get ":id/milestones/:milestone_id" do
authorize! :read_milestone, user_project
- @milestone = user_project.milestones.find(params[:milestone_id])
- present @milestone, with: Entities::Milestone
+ milestone = user_project.milestones.find(params[:milestone_id])
+ present milestone, with: Entities::Milestone
end
- # Create a new project milestone
- #
- # Parameters:
- # id (required) - The ID of the project
- # title (required) - The title of the milestone
- # description (optional) - The description of the milestone
- # due_date (optional) - The due date of the milestone
- # Example Request:
- # POST /projects/:id/milestones
+ desc 'Create a new project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the milestone'
+ use :optional_params
+ end
post ":id/milestones" do
authorize! :admin_milestone, user_project
- required_attributes! [:title]
- attrs = attributes_for_keys [:title, :description, :due_date]
- milestone = ::Milestones::CreateService.new(user_project, current_user, attrs).execute
+
+ milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute
if milestone.valid?
present milestone, with: Entities::Milestone
@@ -70,22 +72,23 @@ module API
end
end
- # Update an existing project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # title (optional) - The title of a milestone
- # description (optional) - The description of a milestone
- # due_date (optional) - The due date of a milestone
- # state_event (optional) - The state event of the milestone (close|activate)
- # Example Request:
- # PUT /projects/:id/milestones/:milestone_id
+ desc 'Update an existing project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ optional :title, type: String, desc: 'The title of the milestone'
+ optional :state_event, type: String, values: %w[close activate],
+ desc: 'The state event of the milestone '
+ use :optional_params
+ at_least_one_of :title, :description, :due_date, :state_event
+ end
put ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_project
- attrs = attributes_for_keys [:title, :description, :due_date, :state_event]
- milestone = user_project.milestones.find(params[:milestone_id])
- milestone = ::Milestones::UpdateService.new(user_project, current_user, attrs).execute(milestone)
+ milestone = user_project.milestones.find(params.delete(:milestone_id))
+
+ milestone_params = declared_params(include_missing: false)
+ milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
if milestone.valid?
present milestone, with: Entities::Milestone
@@ -94,25 +97,24 @@ module API
end
end
- # Get all issues for a single project milestone
- #
- # Parameters:
- # id (required) - The ID of a project
- # milestone_id (required) - The ID of a project milestone
- # Example Request:
- # GET /projects/:id/milestones/:milestone_id/issues
+ desc 'Get all issues for a single project milestone' do
+ success Entities::Issue
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ end
get ":id/milestones/:milestone_id/issues" do
authorize! :read_milestone, user_project
- @milestone = user_project.milestones.find(params[:milestone_id])
+ milestone = user_project.milestones.find(params[:milestone_id])
finder_params = {
project_id: user_project.id,
- milestone_title: @milestone.title
+ milestone_title: milestone.title
}
issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::Issue, current_user: current_user
+ present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index c5c214d4d13..b255b47742b 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -5,23 +5,23 @@ module API
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet]
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
- noteable_id_str = "#{noteable_type.to_s.underscore}_id"
-
- # Get a list of project +noteable+ notes
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes
- # GET /projects/:id/snippets/:noteable_id/notes
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
-
- if can?(current_user, noteable_read_ability_name(@noteable), @noteable)
+
+ desc 'Get a list of project +noteable+ notes' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+
+ if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
# by the current user. By doing this exclusion at this level and not
# at the DB query level (which we cannot in that case), the current
@@ -31,7 +31,7 @@ module API
# paginate() only works with a relation. This could lead to a
# mismatch between the pagination headers info and the actual notes
# array returned, but this is really a edge-case.
- paginate(@noteable.notes).
+ paginate(noteable.notes).
reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
@@ -39,44 +39,40 @@ module API
end
end
- # Get a single +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # note_id (required) - The ID of a note
- # Example Request:
- # GET /projects/:id/issues/:noteable_id/notes/:note_id
- # GET /projects/:id/snippets/:noteable_id/notes/:note_id
- get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
- @note = @noteable.notes.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user)
+ desc 'Get a single +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
+ noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ note = noteable.notes.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
if can_read_note
- present @note, with: Entities::Note
+ present note, with: Entities::Note
else
not_found!("Note")
end
end
- # Create a new +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # body (required) - The content of a note
- # created_at (optional) - The date
- # Example Request:
- # POST /projects/:id/issues/:noteable_id/notes
- # POST /projects/:id/snippets/:noteable_id/notes
- post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
+ desc 'Create a new +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/notes" do
required_attributes! [:body]
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
- noteable_id: params[noteable_id_str]
+ noteable_id: params[:noteable_id]
}
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
@@ -92,19 +88,15 @@ module API
end
end
- # Modify existing +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue or snippet
- # node_id (required) - The ID of a note
- # body (required) - New content of a note
- # Example Request:
- # PUT /projects/:id/issues/:noteable_id/notes/:note_id
- # PUT /projects/:id/snippets/:noteable_id/notes/:node_id
- put ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
- required_attributes! [:body]
-
+ desc 'Update an existing +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
@@ -113,25 +105,23 @@ module API
note: params[:body]
}
- @note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
+ note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
- if @note.valid?
- present @note, with: Entities::Note
+ if note.valid?
+ present note, with: Entities::Note
else
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
- # Delete a +noteable+ note
- #
- # Parameters:
- # id (required) - The ID of a project
- # noteable_id (required) - The ID of an issue, MR, or snippet
- # node_id (required) - The ID of a note
- # Example Request:
- # DELETE /projects/:id/issues/:noteable_id/notes/:note_id
- # DELETE /projects/:id/snippets/:noteable_id/notes/:node_id
- delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
+ desc 'Delete a +noteable+ note' do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index a70a7e71073..c5e9b3ad69b 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -33,10 +33,9 @@ module API
begin
notification_setting.transaction do
new_notification_email = params.delete(:notification_email)
- declared_params = declared(params, include_missing: false).to_h
current_user.update(notification_email: new_notification_email) if new_notification_email
- notification_setting.update(declared_params)
+ notification_setting.update(declared_params(include_missing: false))
end
rescue ArgumentError => e # catch level enum error
render_api_error! e.to_s, 400
@@ -81,9 +80,7 @@ module API
notification_setting = current_user.notification_settings_for(source)
begin
- declared_params = declared(params, include_missing: false).to_h
-
- notification_setting.update(declared_params)
+ notification_setting.update(declared_params(include_missing: false))
rescue ArgumentError => e # catch level enum error
render_api_error! e.to_s, 400
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 2a0c8e1f2c0..e69b0569612 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -22,6 +22,27 @@ module API
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline
end
+
+ desc 'Create a new pipeline' do
+ detail 'This feature was introduced in GitLab 8.14'
+ success Entities::Pipeline
+ end
+ params do
+ requires :ref, type: String, desc: 'Reference'
+ end
+ post ':id/pipeline' do
+ authorize! :create_pipeline, user_project
+
+ new_pipeline = Ci::CreatePipelineService.new(user_project,
+ current_user,
+ declared_params(include_missing: false))
+ .execute(ignore_skip_ci: true, save_on_errors: false)
+ if new_pipeline.persisted?
+ present new_pipeline, with: Entities::Pipeline
+ else
+ render_validation_error!(new_pipeline)
+ end
+ end
desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11'
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index eef343c2ac6..2b36ef7c426 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -51,8 +51,7 @@ module API
use :project_hook_properties
end
post ":id/hooks" do
- new_hook_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
- hook = user_project.hooks.new(new_hook_params)
+ hook = user_project.hooks.new(declared_params(include_missing: false))
if hook.save
present hook, with: Entities::ProjectHook
@@ -71,12 +70,9 @@ module API
use :project_hook_properties
end
put ":id/hooks/:hook_id" do
- hook = user_project.hooks.find(params[:hook_id])
-
- new_params = declared(params, include_missing: false, include_parent_namespaces: false).to_h
- new_params.delete('hook_id')
+ hook = user_project.hooks.find(params.delete(:hook_id))
- if hook.update_attributes(new_params)
+ if hook.update_attributes(declared_params(include_missing: false))
present hook, with: Entities::ProjectHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index f55aceed92c..c287ee34a68 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -1,11 +1,13 @@
require 'mime/types'
module API
- # Projects API
class Repositories < Grape::API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
helpers do
def handle_project_member_errors(errors)
@@ -16,13 +18,14 @@ module API
end
end
- # Get a project repository tree
- #
- # Parameters:
- # id (required) - The ID of a project
- # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used
- # Example Request:
- # GET /projects/:id/repository/tree
+ desc 'Get a project repository tree' do
+ success Entities::RepoTreeObject
+ end
+ params do
+ optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :path, type: String, desc: 'The path of the tree'
+ optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
+ end
get ':id/repository/tree' do
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
@@ -30,27 +33,20 @@ module API
commit = user_project.commit(ref)
not_found!('Tree') unless commit
- tree = user_project.repository.tree(commit.id, path)
+ tree = user_project.repository.tree(commit.id, path, recursive: params[:recursive])
present tree.sorted_entries, with: Entities::RepoTreeObject
end
- # Get a raw file contents
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The commit or branch name
- # filepath (required) - The path to the file to display
- # Example Request:
- # GET /projects/:id/repository/blobs/:sha
+ desc 'Get a raw file contents'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :filepath, type: String, desc: 'The path to the file to display'
+ end
get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do
- required_attributes! [:filepath]
-
- ref = params[:sha]
-
repo = user_project.repository
- commit = repo.commit(ref)
+ commit = repo.commit(params[:sha])
not_found! "Commit" unless commit
blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
@@ -59,20 +55,15 @@ module API
send_git_blob repo, blob
end
- # Get a raw blob contents by blob sha
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (required) - The blob's sha
- # Example Request:
- # GET /projects/:id/repository/raw_blobs/:sha
+ desc 'Get a raw blob contents by blob sha'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ end
get ':id/repository/raw_blobs/:sha' do
- ref = params[:sha]
-
repo = user_project.repository
begin
- blob = Gitlab::Git::Blob.raw(repo, ref)
+ blob = Gitlab::Git::Blob.raw(repo, params[:sha])
rescue
not_found! 'Blob'
end
@@ -82,15 +73,12 @@ module API
send_git_blob repo, blob
end
- # Get a an archive of the repository
- #
- # Parameters:
- # id (required) - The ID of a project
- # sha (optional) - the commit sha to download defaults to the tip of the default branch
- # Example Request:
- # GET /projects/:id/repository/archive
- get ':id/repository/archive',
- requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ desc 'Get an archive of the repository'
+ params do
+ optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
+ optional :format, type: String, desc: 'The archive format'
+ end
+ get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
authorize! :download_code, user_project
begin
@@ -100,27 +88,22 @@ module API
end
end
- # Compare two branches, tags or commits
- #
- # Parameters:
- # id (required) - The ID of a project
- # from (required) - the commit sha or branch name
- # to (required) - the commit sha or branch name
- # Example Request:
- # GET /projects/:id/repository/compare?from=master&to=feature
+ desc 'Compare two branches, tags, or commits' do
+ success Entities::Compare
+ end
+ params do
+ requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
+ requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+ end
get ':id/repository/compare' do
authorize! :download_code, user_project
- required_attributes! [:from, :to]
compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
present compare, with: Entities::Compare
end
- # Get repository contributors
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/contributors
+ desc 'Get repository contributors' do
+ success Entities::Contributor
+ end
get ':id/repository/contributors' do
authorize! :download_code, user_project
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ecc8f2fc5a2..b145cce7e3e 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -1,34 +1,39 @@
module API
- # Runners API
class Runners < Grape::API
before { authenticate! }
resource :runners do
- # Get runners available for user
- #
- # Example Request:
- # GET /runners
+ desc 'Get runners available for user' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online],
+ desc: 'The scope of specific runners to show'
+ end
get do
runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
present paginate(runners), with: Entities::Runner
end
- # Get all runners - shared and specific
- #
- # Example Request:
- # GET /runners/all
+ desc 'Get all runners - shared and specific' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online specific shared],
+ desc: 'The scope of specific runners to show'
+ end
get 'all' do
authenticated_as_admin!
runners = filter_runners(Ci::Runner.all, params[:scope])
present paginate(runners), with: Entities::Runner
end
- # Get runner's details
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # Example Request:
- # GET /runners/:id
+ desc "Get runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
get ':id' do
runner = get_runner(params[:id])
authenticate_show_runner!(runner)
@@ -36,33 +41,35 @@ module API
present runner, with: Entities::RunnerDetails, current_user: current_user
end
- # Update runner's details
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # description (optional) - Runner's description
- # active (optional) - Runner's status
- # tag_list (optional) - Array of tags for runner
- # Example Request:
- # PUT /runners/:id
+ desc "Update runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :active, type: Boolean, desc: 'The state of a runner'
+ optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
+ optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
+ at_least_one_of :description, :active, :tag_list, :run_untagged, :locked
+ end
put ':id' do
- runner = get_runner(params[:id])
+ runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner)
- attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
- if runner.update(attrs)
+ if runner.update(declared_params(include_missing: false))
present runner, with: Entities::RunnerDetails, current_user: current_user
else
render_validation_error!(runner)
end
end
- # Remove runner
- #
- # Parameters:
- # id (required) - The ID of ther runner
- # Example Request:
- # DELETE /runners/:id
+ desc 'Remove a runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
delete ':id' do
runner = get_runner(params[:id])
authenticate_delete_runner!(runner)
@@ -72,28 +79,31 @@ module API
end
end
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
before { authorize_admin_project }
- # Get runners available for project
- #
- # Example Request:
- # GET /projects/:id/runners
+ desc 'Get runners available for project' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: %w[active paused online specific shared],
+ desc: 'The scope of specific runners to show'
+ end
get ':id/runners' do
runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
present paginate(runners), with: Entities::Runner
end
- # Enable runner for project
- #
- # Parameters:
- # id (required) - The ID of the project
- # runner_id (required) - The ID of the runner
- # Example Request:
- # POST /projects/:id/runners/:runner_id
+ desc 'Enable a runner for a project' do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
post ':id/runners' do
- required_attributes! [:runner_id]
-
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
@@ -106,13 +116,12 @@ module API
end
end
- # Disable project's runner
- #
- # Parameters:
- # id (required) - The ID of the project
- # runner_id (required) - The ID of the runner
- # Example Request:
- # DELETE /projects/:id/runners/:runner_id
+ desc "Disable project's runner" do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
delete ':id/runners/:runner_id' do
runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
not_found!('Runner') unless runner_project
diff --git a/lib/api/services.rb b/lib/api/services.rb
index fc8598daa32..4d23499aa39 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,10 +1,10 @@
module API
# Projects API
class Services < Grape::API
- before { authenticate! }
- before { authorize_admin_project }
-
resource :projects do
+ before { authenticate! }
+ before { authorize_admin_project }
+
# Set <service_slug> service for project
#
# Example Request:
@@ -59,5 +59,28 @@ module API
present project_service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
end
end
+
+ resource :projects do
+ desc 'Trigger a slash command' do
+ detail 'Added in GitLab 8.13'
+ end
+ post ':id/services/:service_slug/trigger' do
+ project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
+
+ # This is not accurate, but done to prevent leakage of the project names
+ not_found!('Service') unless project
+
+ service = project_service(project)
+
+ result = service.try(:active?) && service.try(:trigger, params)
+
+ if result
+ status result[:status] || 200
+ present result
+ else
+ not_found!('Service')
+ end
+ end
+ end
end
end
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 55ec66a6d67..d09400b81f5 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -1,15 +1,14 @@
module API
- # Users API
class Session < Grape::API
- # Login to get token
- #
- # Parameters:
- # login (*required) - user login
- # email (*required) - user email
- # password (required) - user password
- #
- # Example Request:
- # POST /session
+ desc 'Login to get token' do
+ success Entities::UserLogin
+ end
+ params do
+ optional :login, type: String, desc: 'The username'
+ optional :email, type: String, desc: 'The email of the user'
+ requires :password, type: String, desc: 'The password of the user'
+ at_least_one_of :login, :email
+ end
post "/session" do
user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index c49e2a21b82..10749b34004 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -9,49 +9,40 @@ module API
'labels' => proc { |id| find_project_label(id) },
}
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :subscribable_id, type: String, desc: 'The ID of a resource'
+ end
resource :projects do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
- type_id_str = :"#{type_singularized}_id"
entity_class = Entities.const_get(type_singularized.camelcase)
- # Subscribe to a resource
- #
- # Parameters:
- # id (required) - The ID of a project
- # subscribable_id (required) - The ID of a resource
- # Example Request:
- # POST /projects/:id/labels/:subscribable_id/subscription
- # POST /projects/:id/issues/:subscribable_id/subscription
- # POST /projects/:id/merge_requests/:subscribable_id/subscription
- post ":id/#{type}/:#{type_id_str}/subscription" do
- resource = instance_exec(params[type_id_str], &finder)
+ desc 'Subscribe to a resource' do
+ success entity_class
+ end
+ post ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
- if resource.subscribed?(current_user)
+ if resource.subscribed?(current_user, user_project)
not_modified!
else
- resource.subscribe(current_user)
- present resource, with: entity_class, current_user: current_user
+ resource.subscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
end
end
- # Unsubscribe from a resource
- #
- # Parameters:
- # id (required) - The ID of a project
- # subscribable_id (required) - The ID of a resource
- # Example Request:
- # DELETE /projects/:id/labels/:subscribable_id/subscription
- # DELETE /projects/:id/issues/:subscribable_id/subscription
- # DELETE /projects/:id/merge_requests/:subscribable_id/subscription
- delete ":id/#{type}/:#{type_id_str}/subscription" do
- resource = instance_exec(params[type_id_str], &finder)
+ desc 'Unsubscribe from a resource' do
+ success entity_class
+ end
+ delete ":id/#{type}/:subscribable_id/subscription" do
+ resource = instance_exec(params[:subscribable_id], &finder)
- if !resource.subscribed?(current_user)
+ if !resource.subscribed?(current_user, user_project)
not_modified!
else
- resource.unsubscribe(current_user)
- present resource, with: entity_class, current_user: current_user
+ resource.unsubscribe(current_user, user_project)
+ present resource, with: entity_class, current_user: current_user, project: user_project
end
end
end
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 32f731c5652..708ec8cfe70 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -27,12 +27,12 @@ module API
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
end
post do
- hook = SystemHook.new declared(params, include_missing: false).to_h
+ hook = SystemHook.new(declared_params(include_missing: false))
if hook.save
present hook, with: Entities::Hook
else
- not_found!
+ render_validation_error!(hook)
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index bf2a199ce21..cd33f9a9903 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -40,10 +40,9 @@ module API
end
post ':id/repository/tags' do
authorize_push_project
- create_params = declared(params)
result = CreateTagService.new(user_project, current_user).
- execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
+ execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
present result[:tag],
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index d1d07394e92..569598fbd2c 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -1,19 +1,18 @@
module API
- # Triggers API
class Triggers < Grape::API
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Trigger a GitLab project build
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # ref (required) - The name of project's branch or tag
- # token (required) - The uniq token of trigger
- # variables (optional) - The list of variables to be injected into build
- # Example Request:
- # POST /projects/:id/trigger/builds
- post ":id/trigger/builds" do
- required_attributes! [:ref, :token]
-
+ desc 'Trigger a GitLab project build' do
+ success Entities::TriggerRequest
+ end
+ params do
+ requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
+ requires :token, type: String, desc: 'The unique token of trigger'
+ optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
+ end
+ post ":id/(ref/:ref/)trigger/builds" do
project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
@@ -22,10 +21,6 @@ module API
# validate variables
variables = params[:variables]
if variables
- unless variables.is_a?(Hash)
- render_api_error!('variables needs to be a hash', 400)
- end
-
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400)
end
@@ -44,31 +39,24 @@ module API
end
end
- # Get triggers list
- #
- # Parameters:
- # id (required) - The ID of a project
- # page (optional) - The page number for pagination
- # per_page (optional) - The value of items per page to show
- # Example Request:
- # GET /projects/:id/triggers
+ desc 'Get triggers list' do
+ success Entities::Trigger
+ end
get ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
triggers = user_project.triggers.includes(:trigger_requests)
- triggers = paginate(triggers)
- present triggers, with: Entities::Trigger
+ present paginate(triggers), with: Entities::Trigger
end
- # Get specific trigger of a project
- #
- # Parameters:
- # id (required) - The ID of a project
- # token (required) - The `token` of a trigger
- # Example Request:
- # GET /projects/:id/triggers/:token
+ desc 'Get specific trigger of a project' do
+ success Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
get ':id/triggers/:token' do
authenticate!
authorize! :admin_build, user_project
@@ -79,12 +67,9 @@ module API
present trigger, with: Entities::Trigger
end
- # Create trigger
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # POST /projects/:id/triggers
+ desc 'Create a trigger' do
+ success Entities::Trigger
+ end
post ':id/triggers' do
authenticate!
authorize! :admin_build, user_project
@@ -94,13 +79,12 @@ module API
present trigger, with: Entities::Trigger
end
- # Delete trigger
- #
- # Parameters:
- # id (required) - The ID of a project
- # token (required) - The `token` of a trigger
- # Example Request:
- # DELETE /projects/:id/triggers/:token
+ desc 'Delete a trigger' do
+ success Entities::Trigger
+ end
+ params do
+ requires :token, type: String, desc: 'The unique token of trigger'
+ end
delete ':id/triggers/:token' do
authenticate!
authorize! :admin_build, user_project
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c28e07a76b7..c07539194ed 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -4,84 +4,93 @@ module API
before { authenticate! }
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
- # Get a users list
- #
- # Example Request:
- # GET /users
- # GET /users?search=Admin
- # GET /users?username=root
+ helpers do
+ params :optional_attributes do
+ optional :skype, type: String, desc: 'The Skype username'
+ optional :linkedin, type: String, desc: 'The LinkedIn username'
+ optional :twitter, type: String, desc: 'The Twitter username'
+ optional :website_url, type: String, desc: 'The website of the user'
+ optional :organization, type: String, desc: 'The organization of the user'
+ optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
+ optional :extern_uid, type: Integer, desc: 'The external authentication provider UID'
+ optional :provider, type: String, desc: 'The external provider'
+ optional :bio, type: String, desc: 'The biography of the user'
+ optional :location, type: String, desc: 'The location of the user'
+ optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
+ optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
+ optional :confirm, type: Boolean, desc: 'Flag indicating the account needs to be confirmed'
+ optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
+ all_or_none_of :extern_uid, :provider
+ end
+ end
+
+ desc 'Get the list of users' do
+ success Entities::UserBasic
+ end
+ params do
+ optional :username, type: String, desc: 'Get a single user with a specific username'
+ optional :search, type: String, desc: 'Search for a username'
+ optional :active, type: Boolean, default: false, desc: 'Filters only active users'
+ optional :external, type: Boolean, default: false, desc: 'Filters only external users'
+ optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users'
+ end
get do
unless can?(current_user, :read_users_list, nil)
render_api_error!("Not authorized.", 403)
end
if params[:username].present?
- @users = User.where(username: params[:username])
+ users = User.where(username: params[:username])
else
- @users = User.all
- @users = @users.active if params[:active].present?
- @users = @users.search(params[:search]) if params[:search].present?
- @users = paginate @users
+ users = User.all
+ users = users.active if params[:active]
+ users = users.search(params[:search]) if params[:search].present?
+ users = users.blocked if params[:blocked]
+ users = users.external if params[:external] && current_user.is_admin?
end
- if current_user.is_admin?
- present @users, with: Entities::UserFull
- else
- present @users, with: Entities::UserBasic
- end
+ entity = current_user.is_admin? ? Entities::UserFull : Entities::UserBasic
+ present paginate(users), with: entity
end
- # Get a single user
- #
- # Parameters:
- # id (required) - The ID of a user
- # Example Request:
- # GET /users/:id
+ desc 'Get a single user' do
+ success Entities::UserBasic
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
get ":id" do
- @user = User.find(params[:id])
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
if current_user && current_user.is_admin?
- present @user, with: Entities::UserFull
- elsif can?(current_user, :read_user, @user)
- present @user, with: Entities::User
+ present user, with: Entities::UserFull
+ elsif can?(current_user, :read_user, user)
+ present user, with: Entities::User
else
render_api_error!("User not found.", 404)
end
end
- # Create user. Available only for admin
- #
- # Parameters:
- # email (required) - Email
- # password (required) - Password
- # name (required) - Name
- # username (required) - Name
- # skype - Skype ID
- # linkedin - Linkedin
- # twitter - Twitter account
- # website_url - Website url
- # organization - Organization
- # projects_limit - Number of projects user can create
- # extern_uid - External authentication provider UID
- # provider - External provider
- # bio - Bio
- # location - Location of the user
- # admin - User is admin - true or false (default)
- # can_create_group - User can create groups - true or false
- # confirm - Require user confirmation - true (default) or false
- # external - Flags the user as external - true or false(default)
- # Example Request:
- # POST /users
+ desc 'Create a user. Available only for admins.' do
+ success Entities::UserFull
+ end
+ params do
+ requires :email, type: String, desc: 'The email of the user'
+ requires :password, type: String, desc: 'The password of the new user'
+ requires :name, type: String, desc: 'The name of the user'
+ requires :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ end
post do
authenticated_as_admin!
- required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
- admin = attrs.delete(:admin)
- confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
- user = User.build_user(attrs)
- user.admin = admin unless admin.nil?
+
+ # Filter out params which are used later
+ identity_attrs = params.slice(:provider, :extern_uid)
+ confirm = params.delete(:confirm)
+
+ user = User.build_user(declared_params(include_missing: false))
user.skip_confirmation! unless confirm
- identity_attrs = attributes_for_keys [:provider, :extern_uid]
if identity_attrs.any?
user.identities.build(identity_attrs)
@@ -102,46 +111,40 @@ module API
end
end
- # Update user. Available only for admin
- #
- # Parameters:
- # email - Email
- # name - Name
- # password - Password
- # skype - Skype ID
- # linkedin - Linkedin
- # twitter - Twitter account
- # website_url - Website url
- # organization - Organization
- # projects_limit - Limit projects each user can create
- # bio - Bio
- # location - Location of the user
- # admin - User is admin - true or false (default)
- # can_create_group - User can create groups - true or false
- # external - Flags the user as external - true or false(default)
- # Example Request:
- # PUT /users/:id
+ desc 'Update a user. Available only for admins.' do
+ success Entities::UserFull
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ optional :email, type: String, desc: 'The email of the user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :name, type: String, desc: 'The name of the user'
+ optional :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ at_least_one_of :email, :password, :name, :username, :skype, :linkedin,
+ :twitter, :website_url, :organization, :projects_limit,
+ :extern_uid, :provider, :bio, :location, :admin,
+ :can_create_group, :confirm, :external
+ end
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
- user = User.find(params[:id])
+ user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
- admin = attrs.delete(:admin)
- user.admin = admin unless admin.nil?
-
- conflict!('Email has already been taken') if attrs[:email] &&
- User.where(email: attrs[:email]).
+ conflict!('Email has already been taken') if params[:email] &&
+ User.where(email: params[:email]).
where.not(id: user.id).count > 0
- conflict!('Username has already been taken') if attrs[:username] &&
- User.where(username: attrs[:username]).
+ conflict!('Username has already been taken') if params[:username] &&
+ User.where(username: params[:username]).
where.not(id: user.id).count > 0
- identity_attrs = attributes_for_keys [:provider, :extern_uid]
+ identity_attrs = params.slice(:provider, :extern_uid)
+
if identity_attrs.any?
identity = user.identities.find_by(provider: identity_attrs[:provider])
+
if identity
identity.update_attributes(identity_attrs)
else
@@ -150,28 +153,33 @@ module API
end
end
- if user.update_attributes(attrs)
+ # Delete already handled parameters
+ params.delete(:extern_uid)
+ params.delete(:provider)
+
+ if user.update_attributes(declared_params(include_missing: false))
present user, with: Entities::UserFull
else
render_validation_error!(user)
end
end
- # Add ssh key to a specified user. Only available to admin users.
- #
- # Parameters:
- # id (required) - The ID of a user
- # key (required) - New SSH Key
- # title (required) - New SSH Key's title
- # Example Request:
- # POST /users/:id/keys
+ desc 'Add an SSH key to a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key, type: String, desc: 'The new SSH key'
+ requires :title, type: String, desc: 'The title of the new SSH key'
+ end
post ":id/keys" do
authenticated_as_admin!
- required_attributes! [:title, :key]
- user = User.find(params[:id])
- attrs = attributes_for_keys [:title, :key]
- key = user.keys.new attrs
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ key = user.keys.new(declared_params(include_missing: false))
+
if key.save
present key, with: Entities::SSHKey
else
@@ -179,55 +187,55 @@ module API
end
end
- # Get ssh keys of a specified user. Only available to admin users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # Example Request:
- # GET /users/:uid/keys
- get ':uid/keys' do
+ desc 'Get the SSH keys of a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ get ':id/keys' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
present user.keys, with: Entities::SSHKey
end
- # Delete existing ssh key of a specified user. Only available to admin
- # users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # id (required) - SSH Key ID
- # Example Request:
- # DELETE /users/:uid/keys/:id
- delete ':uid/keys/:id' do
+ desc 'Delete an existing SSH key from a specified user. Available only for admins.' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete ':id/keys/:key_id' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
- begin
- key = user.keys.find params[:id]
- key.destroy
- rescue ActiveRecord::RecordNotFound
- not_found!('Key')
- end
+ key = user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: Entities::SSHKey
end
- # Add email to a specified user. Only available to admin users.
- #
- # Parameters:
- # id (required) - The ID of a user
- # email (required) - Email address
- # Example Request:
- # POST /users/:id/emails
+ desc 'Add an email address to a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :email, type: String, desc: 'The email of the user'
+ end
post ":id/emails" do
authenticated_as_admin!
- required_attributes! [:email]
- user = User.find(params[:id])
- attrs = attributes_for_keys [:email]
- email = user.emails.new attrs
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ email = user.emails.new(declared_params(include_missing: false))
+
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
@@ -236,101 +244,94 @@ module API
end
end
- # Get emails of a specified user. Only available to admin users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # Example Request:
- # GET /users/:uid/emails
- get ':uid/emails' do
+ desc 'Get the emails addresses of a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
+ get ':id/emails' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
present user.emails, with: Entities::Email
end
- # Delete existing email of a specified user. Only available to admin
- # users.
- #
- # Parameters:
- # uid (required) - The ID of a user
- # id (required) - Email ID
- # Example Request:
- # DELETE /users/:uid/emails/:id
- delete ':uid/emails/:id' do
+ desc 'Delete an email address of a specified user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ delete ':id/emails/:email_id' do
authenticated_as_admin!
- user = User.find_by(id: params[:uid])
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
- begin
- email = user.emails.find params[:id]
- email.destroy
+ email = user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
- user.update_secondary_emails!
- rescue ActiveRecord::RecordNotFound
- not_found!('Email')
- end
+ email.destroy
+ user.update_secondary_emails!
end
- # Delete user. Available only for admin
- #
- # Example Request:
- # DELETE /users/:id
+ desc 'Delete a user. Available only for admins.' do
+ success Entities::Email
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
delete ":id" do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if user
- DeleteUserService.new(current_user).execute(user)
- else
- not_found!('User')
- end
+ DeleteUserService.new(current_user).execute(user)
end
- # Block user. Available only for admin
- #
- # Example Request:
- # PUT /users/:id/block
+ desc 'Block a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
put ':id/block' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if !user
- not_found!('User')
- elsif !user.ldap_blocked?
+ if !user.ldap_blocked?
user.block
else
forbidden!('LDAP blocked users cannot be modified by the API')
end
end
- # Unblock user. Available only for admin
- #
- # Example Request:
- # PUT /users/:id/unblock
+ desc 'Unblock a user. Available only for admins.'
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ end
put ':id/unblock' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
+ not_found!('User') unless user
- if !user
- not_found!('User')
- elsif user.ldap_blocked?
+ if user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
else
user.activate
end
end
- desc 'Get contribution events of a specified user' do
+ desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
- requires :id, type: String, desc: 'The user ID'
+ requires :id, type: Integer, desc: 'The ID of the user'
end
get ':id/events' do
- user = User.find_by(id: declared(params).id)
+ user = User.find_by(id: params[:id])
not_found!('User') unless user
events = user.events.
@@ -344,43 +345,43 @@ module API
end
resource :user do
- # Get currently authenticated user
- #
- # Example Request:
- # GET /user
+ desc 'Get the currently authenticated user' do
+ success Entities::UserFull
+ end
get do
- present @current_user, with: Entities::UserFull
+ present current_user, with: Entities::UserFull
end
- # Get currently authenticated user's keys
- #
- # Example Request:
- # GET /user/keys
+ desc "Get the currently authenticated user's SSH keys" do
+ success Entities::SSHKey
+ end
get "keys" do
present current_user.keys, with: Entities::SSHKey
end
- # Get single key owned by currently authenticated user
- #
- # Example Request:
- # GET /user/keys/:id
- get "keys/:id" do
- key = current_user.keys.find params[:id]
+ desc 'Get a single key owned by currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ get "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
present key, with: Entities::SSHKey
end
- # Add new ssh key to currently authenticated user
- #
- # Parameters:
- # key (required) - New SSH Key
- # title (required) - New SSH Key's title
- # Example Request:
- # POST /user/keys
+ desc 'Add a new SSH key to the currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new SSH key'
+ requires :title, type: String, desc: 'The title of the new SSH key'
+ end
post "keys" do
- required_attributes! [:title, :key]
+ key = current_user.keys.new(declared_params)
- attrs = attributes_for_keys [:title, :key]
- key = current_user.keys.new attrs
if key.save
present key, with: Entities::SSHKey
else
@@ -388,48 +389,48 @@ module API
end
end
- # Delete existing ssh key of currently authenticated user
- #
- # Parameters:
- # id (required) - SSH Key ID
- # Example Request:
- # DELETE /user/keys/:id
- delete "keys/:id" do
- begin
- key = current_user.keys.find params[:id]
- key.destroy
- rescue
- end
+ desc 'Delete an SSH key from the currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete "keys/:key_id" do
+ key = current_user.keys.find_by(id: params[:key_id])
+ not_found!('Key') unless key
+
+ present key.destroy, with: Entities::SSHKey
end
- # Get currently authenticated user's emails
- #
- # Example Request:
- # GET /user/emails
+ desc "Get the currently authenticated user's email addresses" do
+ success Entities::Email
+ end
get "emails" do
present current_user.emails, with: Entities::Email
end
- # Get single email owned by currently authenticated user
- #
- # Example Request:
- # GET /user/emails/:id
- get "emails/:id" do
- email = current_user.emails.find params[:id]
+ desc 'Get a single email address owned by the currently authenticated user' do
+ success Entities::Email
+ end
+ params do
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ get "emails/:email_id" do
+ email = current_user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
+
present email, with: Entities::Email
end
- # Add new email to currently authenticated user
- #
- # Parameters:
- # email (required) - Email address
- # Example Request:
- # POST /user/emails
+ desc 'Add new email address to the currently authenticated user' do
+ success Entities::Email
+ end
+ params do
+ requires :email, type: String, desc: 'The new email'
+ end
post "emails" do
- required_attributes! [:email]
+ email = current_user.emails.new(declared_params)
- attrs = attributes_for_keys [:email]
- email = current_user.emails.new attrs
if email.save
NotificationService.new.new_email(email)
present email, with: Entities::Email
@@ -438,20 +439,16 @@ module API
end
end
- # Delete existing email of currently authenticated user
- #
- # Parameters:
- # id (required) - EMail ID
- # Example Request:
- # DELETE /user/emails/:id
- delete "emails/:id" do
- begin
- email = current_user.emails.find params[:id]
- email.destroy
+ desc 'Delete an email address from the currently authenticated user'
+ params do
+ requires :email_id, type: Integer, desc: 'The ID of the email'
+ end
+ delete "emails/:email_id" do
+ email = current_user.emails.find_by(id: params[:email_id])
+ not_found!('Email') unless email
- current_user.update_secondary_emails!
- rescue
- end
+ email.destroy
+ current_user.update_secondary_emails!
end
end
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 799b83b1069..80c844baecd 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -71,6 +71,14 @@ module Banzai
@doc = parse_html(rinku)
end
+ # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
+ def contains_unsafe?(scheme)
+ return false unless scheme
+
+ scheme = scheme.strip.downcase
+ Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
+ end
+
# Autolinks any text matching LINK_PATTERN that Rinku didn't already
# replace
def text_parse
@@ -89,17 +97,27 @@ module Banzai
doc
end
- def autolink_filter(text)
- text.gsub(LINK_PATTERN) do |match|
- # Remove any trailing HTML entities and store them for appending
- # outside the link element. The entity must be marked HTML safe in
- # order to be output literally rather than escaped.
- match.gsub!(/((?:&[\w#]+;)+)\z/, '')
- dropped = ($1 || '').html_safe
-
- options = link_options.merge(href: match)
- content_tag(:a, match, options) + dropped
+ def autolink_match(match)
+ # start by stripping out dangerous links
+ begin
+ uri = Addressable::URI.parse(match)
+ return match if contains_unsafe?(uri.scheme)
+ rescue Addressable::URI::InvalidURIError
+ return match
end
+
+ # Remove any trailing HTML entities and store them for appending
+ # outside the link element. The entity must be marked HTML safe in
+ # order to be output literally rather than escaped.
+ match.gsub!(/((?:&[\w#]+;)+)\z/, '')
+ dropped = ($1 || '').html_safe
+
+ options = link_options.merge(href: match)
+ content_tag(:a, match, options) + dropped
+ end
+
+ def autolink_filter(text)
+ text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
end
def link_options
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index f5d110e987b..d8a855ec1fe 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -63,12 +63,7 @@ module Banzai
nodes.select do |node|
if node.has_attribute?(project_attr)
node_id = node.attr(project_attr).to_i
-
- if project && project.id == node_id
- true
- else
- can?(user, :read_project, projects[node_id])
- end
+ can_read_reference?(user, projects[node_id])
else
true
end
@@ -226,6 +221,15 @@ module Banzai
attr_reader :current_user, :project
+ # When a feature is disabled or visible only for
+ # team members we should not allow team members
+ # see reference comments.
+ # Override this method on subclasses
+ # to check if user can read resource
+ def can_read_reference?(user, ref_project)
+ raise NotImplementedError
+ end
+
def lazy(&block)
Gitlab::Lazy.new(&block)
end
diff --git a/lib/banzai/reference_parser/commit_parser.rb b/lib/banzai/reference_parser/commit_parser.rb
index 0fee9d267de..8c54a041cb8 100644
--- a/lib/banzai/reference_parser/commit_parser.rb
+++ b/lib/banzai/reference_parser/commit_parser.rb
@@ -29,6 +29,12 @@ module Banzai
commits
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :download_code, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/commit_range_parser.rb b/lib/banzai/reference_parser/commit_range_parser.rb
index 69d01f8db15..0878b6afba3 100644
--- a/lib/banzai/reference_parser/commit_range_parser.rb
+++ b/lib/banzai/reference_parser/commit_range_parser.rb
@@ -33,6 +33,12 @@ module Banzai
range.valid_commits? ? range : nil
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :download_code, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
index a1264db2111..6e7b7669578 100644
--- a/lib/banzai/reference_parser/external_issue_parser.rb
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -20,6 +20,12 @@ module Banzai
def issue_ids_per_project(nodes)
gather_attributes_per_project(nodes, self.class.data_attribute)
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_issue, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/label_parser.rb b/lib/banzai/reference_parser/label_parser.rb
index e5d1eb11d7f..aa76c64ac5f 100644
--- a/lib/banzai/reference_parser/label_parser.rb
+++ b/lib/banzai/reference_parser/label_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Label
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_label, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index c9a9ca79c09..40451947e6c 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
MergeRequest.includes(:author, :assignee, :target_project)
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_merge_request, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/milestone_parser.rb b/lib/banzai/reference_parser/milestone_parser.rb
index a000ac61e5c..d3968d6b229 100644
--- a/lib/banzai/reference_parser/milestone_parser.rb
+++ b/lib/banzai/reference_parser/milestone_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Milestone
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_milestone, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/snippet_parser.rb b/lib/banzai/reference_parser/snippet_parser.rb
index fa71b3c952a..63b592137bb 100644
--- a/lib/banzai/reference_parser/snippet_parser.rb
+++ b/lib/banzai/reference_parser/snippet_parser.rb
@@ -6,6 +6,12 @@ module Banzai
def references_relation
Snippet
end
+
+ private
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_project_snippet, ref_project)
+ end
end
end
end
diff --git a/lib/banzai/reference_parser/user_parser.rb b/lib/banzai/reference_parser/user_parser.rb
index 863f5725d3b..7adaffa19c1 100644
--- a/lib/banzai/reference_parser/user_parser.rb
+++ b/lib/banzai/reference_parser/user_parser.rb
@@ -30,22 +30,36 @@ module Banzai
nodes.each do |node|
if node.has_attribute?(group_attr)
- node_group = groups[node.attr(group_attr).to_i]
-
- if node_group &&
- can?(user, :read_group, node_group)
- visible << node
- end
- # Remaining nodes will be processed by the parent class'
- # implementation of this method.
+ next unless can_read_group_reference?(node, user, groups)
+ visible << node
+ elsif can_read_project_reference?(node)
+ visible << node
else
remaining << node
end
end
+ # If project does not belong to a group
+ # and does not have the same project id as the current project
+ # base class will check if user can read the project that contains
+ # the user reference.
visible + super(current_user, remaining)
end
+ # Check if project belongs to a group which
+ # user can read.
+ def can_read_group_reference?(node, user, groups)
+ node_group = groups[node.attr('data-group').to_i]
+
+ node_group && can?(user, :read_group, node_group)
+ end
+
+ def can_read_project_reference?(node)
+ node_id = node.attr('data-project').to_i
+
+ project && project.id == node_id
+ end
+
def nodes_user_can_reference(current_user, nodes)
project_attr = 'data-project'
author_attr = 'data-author'
@@ -88,6 +102,10 @@ module Banzai
collection_objects_for_ids(Project, ids).
flat_map { |p| p.team.members.to_a }
end
+
+ def can_read_reference?(user, ref_project)
+ can?(user, :read_project, ref_project)
+ end
end
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 3e33c9399e2..fef652cb975 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -2,7 +2,7 @@ module Ci
class GitlabCiYamlProcessor
class ValidationError < StandardError; end
- include Gitlab::Ci::Config::Node::LegacyValidationHelpers
+ include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
attr_reader :path, :cache, :stages, :jobs
diff --git a/lib/constraints/constrainer_helper.rb b/lib/constraints/constrainer_helper.rb
new file mode 100644
index 00000000000..ab07a6793d9
--- /dev/null
+++ b/lib/constraints/constrainer_helper.rb
@@ -0,0 +1,15 @@
+module ConstrainerHelper
+ def extract_resource_path(path)
+ id = path.dup
+ id.sub!(/\A#{relative_url_root}/, '') if relative_url_root
+ id.sub(/\A\/+/, '').sub(/\/+\z/, '').sub(/.atom\z/, '')
+ end
+
+ private
+
+ def relative_url_root
+ if defined?(Gitlab::Application.config.relative_url_root)
+ Gitlab::Application.config.relative_url_root
+ end
+ end
+end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index ca39b1961ae..2af6e1a11c8 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -1,7 +1,15 @@
-require 'constraints/namespace_url_constrainer'
+require_relative 'constrainer_helper'
-class GroupUrlConstrainer < NamespaceUrlConstrainer
- def find_resource(id)
- Group.find_by_path(id)
+class GroupUrlConstrainer
+ include ConstrainerHelper
+
+ def matches?(request)
+ id = extract_resource_path(request.path)
+
+ if id =~ Gitlab::Regex.namespace_regex
+ Group.find_by(path: id).present?
+ else
+ false
+ end
end
end
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
deleted file mode 100644
index 91b70143f11..00000000000
--- a/lib/constraints/namespace_url_constrainer.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-class NamespaceUrlConstrainer
- def matches?(request)
- id = request.path
- id = id.sub(/\A#{relative_url_root}/, '') if relative_url_root
- id = id.sub(/\A\/+/, '').split('/').first
- id = id.sub(/.atom\z/, '') if id
-
- if id =~ Gitlab::Regex.namespace_regex
- find_resource(id)
- end
- end
-
- def find_resource(id)
- Namespace.find_by_path(id)
- end
-
- private
-
- def relative_url_root
- if defined?(Gitlab::Application.config.relative_url_root)
- Gitlab::Application.config.relative_url_root
- end
- end
-end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
index 504a0f5d93e..4d722ad5af2 100644
--- a/lib/constraints/user_url_constrainer.rb
+++ b/lib/constraints/user_url_constrainer.rb
@@ -1,7 +1,15 @@
-require 'constraints/namespace_url_constrainer'
+require_relative 'constrainer_helper'
-class UserUrlConstrainer < NamespaceUrlConstrainer
- def find_resource(id)
- User.find_by('lower(username) = ?', id.downcase)
+class UserUrlConstrainer
+ include ConstrainerHelper
+
+ def matches?(request)
+ id = extract_resource_path(request.path)
+
+ if id =~ Gitlab::Regex.namespace_regex
+ User.find_by('lower(username) = ?', id.downcase).present?
+ else
+ false
+ end
end
end
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 9b74364849e..82551f1f222 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -106,7 +106,7 @@ module ExtractsPath
# resolved (e.g., when a user inserts an invalid path or ref).
def assign_ref_vars
# assign allowed options
- allowed_options = ["filter_ref", "extended_sha1"]
+ allowed_options = ["filter_ref"]
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
@@ -114,17 +114,13 @@ module ExtractsPath
@ref, @path = extract_ref(@id)
@repo = @project.repository
- if @options[:extended_sha1].present?
- @commit = @repo.commit(@options[:extended_sha1])
- else
- @commit = @repo.commit(@ref)
+ @commit = @repo.commit(@ref)
- if @path.empty? && !@commit && @id.ends_with?('.atom')
- @id = @ref = extract_ref_without_atom(@id)
- @commit = @repo.commit(@ref)
+ if @path.empty? && !@commit && @id.ends_with?('.atom')
+ @id = @ref = extract_ref_without_atom(@id)
+ @commit = @repo.commit(@ref)
- request.format = :atom if @commit
- end
+ request.format = :atom if @commit
end
raise InvalidPathError unless @commit
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
new file mode 100644
index 00000000000..e59d69b72b9
--- /dev/null
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module ChatCommands
+ class BaseCommand
+ QUERY_LIMIT = 5
+
+ def self.match(_text)
+ raise NotImplementedError
+ end
+
+ def self.help_message
+ raise NotImplementedError
+ end
+
+ def self.available?(_project)
+ raise NotImplementedError
+ end
+
+ def self.allowed?(_user, _ability)
+ true
+ end
+
+ def self.can?(object, action, subject)
+ Ability.allowed?(object, action, subject)
+ end
+
+ def execute(_)
+ raise NotImplementedError
+ end
+
+ def collection
+ raise NotImplementedError
+ end
+
+ attr_accessor :project, :current_user, :params
+
+ def initialize(project, user, params = {})
+ @project, @current_user, @params = project, user, params.dup
+ end
+
+ private
+
+ def find_by_iid(iid)
+ resource = collection.find_by(iid: iid)
+
+ readable?(resource) ? resource : nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
new file mode 100644
index 00000000000..5f131703d40
--- /dev/null
+++ b/lib/gitlab/chat_commands/command.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ChatCommands
+ class Command < BaseCommand
+ COMMANDS = [
+ Gitlab::ChatCommands::IssueShow,
+ Gitlab::ChatCommands::IssueCreate,
+ ].freeze
+
+ def execute
+ command, match = match_command
+
+ if command
+ if command.allowed?(project, current_user)
+ present command.new(project, current_user, params).execute(match)
+ else
+ access_denied
+ end
+ else
+ help(help_messages)
+ end
+ end
+
+ private
+
+ def match_command
+ match = nil
+ service = available_commands.find do |klass|
+ match = klass.match(command)
+ end
+
+ [service, match]
+ end
+
+ def help_messages
+ available_commands.map(&:help_message)
+ end
+
+ def available_commands
+ COMMANDS.select do |klass|
+ klass.available?(project)
+ end
+ end
+
+ def command
+ params[:text]
+ end
+
+ def help(messages)
+ Mattermost::Presenter.help(messages, params[:command])
+ end
+
+ def access_denied
+ Mattermost::Presenter.access_denied
+ end
+
+ def present(resource)
+ Mattermost::Presenter.present(resource)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_command.rb b/lib/gitlab/chat_commands/issue_command.rb
new file mode 100644
index 00000000000..f1bc36239d5
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_command.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ChatCommands
+ class IssueCommand < BaseCommand
+ def self.available?(project)
+ project.issues_enabled? && project.default_issues_tracker?
+ end
+
+ def collection
+ project.issues
+ end
+
+ def readable?(issue)
+ self.class.can?(current_user, :read_issue, issue)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
new file mode 100644
index 00000000000..98338ebfa27
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -0,0 +1,24 @@
+module Gitlab
+ module ChatCommands
+ class IssueCreate < IssueCommand
+ def self.match(text)
+ /\Aissue\s+create\s+(?<title>[^\n]*)\n*(?<description>.*)\z/.match(text)
+ end
+
+ def self.help_message
+ 'issue create <title>\n<description>'
+ end
+
+ def self.allowed?(project, user)
+ can?(user, :create_issue, project)
+ end
+
+ def execute(match)
+ title = match[:title]
+ description = match[:description]
+
+ Issues::CreateService.new(project, current_user, title: title, description: description).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
new file mode 100644
index 00000000000..f5bceb038e5
--- /dev/null
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module ChatCommands
+ class IssueShow < IssueCommand
+ def self.match(text)
+ /\Aissue\s+show\s+(?<iid>\d+)/.match(text)
+ end
+
+ def self.help_message
+ "issue show <id>"
+ end
+
+ def execute(match)
+ find_by_iid(match[:iid])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb
new file mode 100644
index 00000000000..1b081aa9b1d
--- /dev/null
+++ b/lib/gitlab/chat_name_token.rb
@@ -0,0 +1,45 @@
+require 'json'
+
+module Gitlab
+ class ChatNameToken
+ attr_reader :token
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 10.minutes
+
+ def initialize(token = new_token)
+ @token = token
+ end
+
+ def get
+ Gitlab::Redis.with do |redis|
+ data = redis.get(redis_key)
+ JSON.parse(data, symbolize_names: true) if data
+ end
+ end
+
+ def store!(params)
+ Gitlab::Redis.with do |redis|
+ params = params.to_json
+ redis.set(redis_key, params, ex: EXPIRY_TIME)
+ token
+ end
+ end
+
+ def delete
+ Gitlab::Redis.with do |redis|
+ redis.del(redis_key)
+ end
+ end
+
+ private
+
+ def new_token
+ Devise.friendly_token(TOKEN_LENGTH)
+ end
+
+ def redis_key
+ "gitlab:chat_names:#{token}"
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index bbfa6cf7d05..f7ff7ea212e 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -4,16 +4,10 @@ module Gitlab
# Base GitLab CI Configuration facade
#
class Config
- ##
- # Temporary delegations that should be removed after refactoring
- #
- delegate :before_script, :image, :services, :after_script, :variables,
- :stages, :cache, :jobs, to: :@global
-
def initialize(config)
@config = Loader.new(config).load!
- @global = Node::Global.new(@config)
+ @global = Entry::Global.new(@config)
@global.compose!
end
@@ -28,6 +22,41 @@ module Gitlab
def to_hash
@config
end
+
+ ##
+ # Temporary method that should be removed after refactoring
+ #
+ def before_script
+ @global.before_script_value
+ end
+
+ def image
+ @global.image_value
+ end
+
+ def services
+ @global.services_value
+ end
+
+ def after_script
+ @global.after_script_value
+ end
+
+ def variables
+ @global.variables_value
+ end
+
+ def stages
+ @global.stages_value
+ end
+
+ def cache
+ @global.cache_value
+ end
+
+ def jobs
+ @global.jobs_value
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index 844bd2fe998..b756b0d4555 100644
--- a/lib/gitlab/ci/config/node/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a configuration of job artifacts.
#
- class Artifacts < Entry
+ class Artifacts < Node
include Validatable
include Attributable
diff --git a/lib/gitlab/ci/config/node/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
index 221b666f9f6..1c8b55ee4c4 100644
--- a/lib/gitlab/ci/config/node/attributable.rb
+++ b/lib/gitlab/ci/config/entry/attributable.rb
@@ -1,7 +1,7 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
module Attributable
extend ActiveSupport::Concern
diff --git a/lib/gitlab/ci/config/node/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
index 84b03ee7832..f3357f85b99 100644
--- a/lib/gitlab/ci/config/node/boolean.rb
+++ b/lib/gitlab/ci/config/entry/boolean.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a boolean value.
#
- class Boolean < Entry
+ class Boolean < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index b4bda2841ac..7653cab668b 100644
--- a/lib/gitlab/ci/config/node/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a cache configuration
#
- class Cache < Entry
+ class Cache < Node
include Configurable
ALLOWED_KEYS = %i[key untracked paths]
@@ -14,13 +14,13 @@ module Gitlab
validates :config, allowed_keys: ALLOWED_KEYS
end
- node :key, Node::Key,
+ entry :key, Entry::Key,
description: 'Cache key used to define a cache affinity.'
- node :untracked, Node::Boolean,
+ entry :untracked, Entry::Boolean,
description: 'Cache all untracked files.'
- node :paths, Node::Paths,
+ entry :paths, Entry::Paths,
description: 'Specify which paths should be cached across builds.'
end
end
diff --git a/lib/gitlab/ci/config/node/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index d7657ae314b..65d19db249c 100644
--- a/lib/gitlab/ci/config/node/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a job script.
#
- class Commands < Entry
+ class Commands < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 6b7ab2fdaf2..833ae4a0ff3 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -1,7 +1,7 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# This mixin is responsible for adding DSL, which purpose is to
# simplifly process of adding child nodes.
@@ -48,8 +48,8 @@ module Gitlab
private # rubocop:disable Lint/UselessAccessModifier
- def node(key, node, metadata)
- factory = Node::Factory.new(node)
+ def entry(key, entry, metadata)
+ factory = Entry::Factory.new(entry)
.with(description: metadata[:description])
(@nodes ||= {}).merge!(key.to_sym => factory)
@@ -66,8 +66,6 @@ module Gitlab
@entries[symbol].value
end
-
- alias_method symbol.to_sym, "#{symbol}_value".to_sym
end
end
end
diff --git a/lib/gitlab/ci/config/node/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 9a95ef43628..b7b4b91eb51 100644
--- a/lib/gitlab/ci/config/node/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents an environment.
#
- class Environment < Entry
+ class Environment < Node
include Validatable
ALLOWED_KEYS = %i[name url action on_stop]
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
index 5387f29ad59..9f5e393d191 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/entry/factory.rb
@@ -1,15 +1,15 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
- # Factory class responsible for fabricating node entry objects.
+ # Factory class responsible for fabricating entry objects.
#
class Factory
class InvalidFactory < StandardError; end
- def initialize(node)
- @node = node
+ def initialize(entry)
+ @entry = entry
@metadata = {}
@attributes = {}
end
@@ -37,11 +37,11 @@ module Gitlab
# See issue #18775.
#
if @value.nil?
- Node::Unspecified.new(
+ Entry::Unspecified.new(
fabricate_unspecified
)
else
- fabricate(@node, @value)
+ fabricate(@entry, @value)
end
end
@@ -49,21 +49,21 @@ module Gitlab
def fabricate_unspecified
##
- # If node has a default value we fabricate concrete node
+ # If entry has a default value we fabricate concrete node
# with default value.
#
- if @node.default.nil?
- fabricate(Node::Undefined)
+ if @entry.default.nil?
+ fabricate(Entry::Undefined)
else
- fabricate(@node, @node.default)
+ fabricate(@entry, @entry.default)
end
end
- def fabricate(node, value = nil)
- node.new(value, @metadata).tap do |entry|
- entry.key = @attributes[:key]
- entry.parent = @attributes[:parent]
- entry.description = @attributes[:description]
+ def fabricate(entry, value = nil)
+ entry.new(value, @metadata).tap do |node|
+ node.key = @attributes[:key]
+ node.parent = @attributes[:parent]
+ node.description = @attributes[:description]
end
end
end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/entry/global.rb
index 2a2943c9288..a4ec8f0ff2f 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -1,36 +1,36 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
- # This class represents a global entry - root node for entire
+ # This class represents a global entry - root Entry for entire
# GitLab CI Configuration file.
#
- class Global < Entry
+ class Global < Node
include Configurable
- node :before_script, Node::Script,
+ entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.'
- node :image, Node::Image,
+ entry :image, Entry::Image,
description: 'Docker image that will be used to execute jobs.'
- node :services, Node::Services,
+ entry :services, Entry::Services,
description: 'Docker images that will be linked to the container.'
- node :after_script, Node::Script,
+ entry :after_script, Entry::Script,
description: 'Script that will be executed after each job.'
- node :variables, Node::Variables,
+ entry :variables, Entry::Variables,
description: 'Environment variables that will be used.'
- node :stages, Node::Stages,
+ entry :stages, Entry::Stages,
description: 'Configuration of stages for this pipeline.'
- node :types, Node::Stages,
+ entry :types, Entry::Stages,
description: 'Deprecated: stages for this pipeline.'
- node :cache, Node::Cache,
+ entry :cache, Entry::Cache,
description: 'Configure caching between build jobs.'
helpers :before_script, :image, :services, :after_script,
@@ -46,7 +46,7 @@ module Gitlab
private
def compose_jobs!
- factory = Node::Factory.new(Node::Jobs)
+ factory = Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys))
.with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline')
diff --git a/lib/gitlab/ci/config/node/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
index fe4ee8a7fc6..6fc3aa385bc 100644
--- a/lib/gitlab/ci/config/node/hidden.rb
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
- # Entry that represents a hidden CI/CD job.
+ # Entry that represents a hidden CI/CD key.
#
- class Hidden < Entry
+ class Hidden < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 5d3c7c5eab0..b5050257688 100644
--- a/lib/gitlab/ci/config/node/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a Docker image.
#
- class Image < Entry
+ class Image < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 603334d6793..20dcc024b4e 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a concrete CI/CD job.
#
- class Job < Entry
+ class Job < Node
include Configurable
include Attributable
@@ -13,12 +13,10 @@ module Gitlab
type stage when artifacts cache dependencies before_script
after_script variables environment]
- attributes :tags, :allow_failure, :when, :dependencies
-
validations do
validates :config, allowed_keys: ALLOWED_KEYS
-
validates :config, presence: true
+ validates :script, presence: true
validates :name, presence: true
validates :name, type: Symbol
@@ -34,49 +32,51 @@ module Gitlab
end
end
- node :before_script, Node::Script,
+ entry :before_script, Entry::Script,
description: 'Global before script overridden in this job.'
- node :script, Node::Commands,
+ entry :script, Entry::Commands,
description: 'Commands that will be executed in this job.'
- node :stage, Node::Stage,
+ entry :stage, Entry::Stage,
description: 'Pipeline stage this job will be executed into.'
- node :type, Node::Stage,
+ entry :type, Entry::Stage,
description: 'Deprecated: stage this job will be executed into.'
- node :after_script, Node::Script,
+ entry :after_script, Entry::Script,
description: 'Commands that will be executed when finishing job.'
- node :cache, Node::Cache,
+ entry :cache, Entry::Cache,
description: 'Cache definition for this job.'
- node :image, Node::Image,
+ entry :image, Entry::Image,
description: 'Image that will be used to execute this job.'
- node :services, Node::Services,
+ entry :services, Entry::Services,
description: 'Services that will be used to execute this job.'
- node :only, Node::Trigger,
+ entry :only, Entry::Trigger,
description: 'Refs policy this job will be executed for.'
- node :except, Node::Trigger,
+ entry :except, Entry::Trigger,
description: 'Refs policy this job will be executed for.'
- node :variables, Node::Variables,
+ entry :variables, Entry::Variables,
description: 'Environment variables available for this job.'
- node :artifacts, Node::Artifacts,
+ entry :artifacts, Entry::Artifacts,
description: 'Artifacts configuration for this job.'
- node :environment, Node::Environment,
+ entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts, :commands, :environment
+ attributes :script, :tags, :allow_failure, :when, :dependencies
+
def compose!(deps = nil)
super do
if type_defined? && !stage_defined?
@@ -118,20 +118,20 @@ module Gitlab
def to_hash
{ name: name,
- before_script: before_script,
- script: script,
+ before_script: before_script_value,
+ script: script_value,
commands: commands,
- image: image,
- services: services,
- stage: stage,
- cache: cache,
- only: only,
- except: except,
- variables: variables_defined? ? variables : nil,
- environment: environment_defined? ? environment : nil,
- environment_name: environment_defined? ? environment[:name] : nil,
- artifacts: artifacts,
- after_script: after_script }
+ image: image_value,
+ services: services_value,
+ stage: stage_value,
+ cache: cache_value,
+ only: only_value,
+ except: except_value,
+ variables: variables_defined? ? variables_value : nil,
+ environment: environment_defined? ? environment_value : nil,
+ environment_name: environment_defined? ? environment_value[:name] : nil,
+ artifacts: artifacts_value,
+ after_script: after_script_value }
end
end
end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index d10e80d1a7d..5671a09480b 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a set of jobs.
#
- class Jobs < Entry
+ class Jobs < Node
include Validatable
validations do
@@ -29,9 +29,9 @@ module Gitlab
def compose!(deps = nil)
super do
@config.each do |name, config|
- node = hidden?(name) ? Node::Hidden : Node::Job
+ node = hidden?(name) ? Entry::Hidden : Entry::Job
- factory = Node::Factory.new(node)
+ factory = Entry::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
diff --git a/lib/gitlab/ci/config/node/key.rb b/lib/gitlab/ci/config/entry/key.rb
index f8b461ca098..0e4c9fe6edc 100644
--- a/lib/gitlab/ci/config/node/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a key.
#
- class Key < Entry
+ class Key < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index 0c291efe6a5..f01975aab5c 100644
--- a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -1,7 +1,7 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
module LegacyValidationHelpers
private
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/entry/node.rb
index 8717eabf81e..5eef2868cd6 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Base abstract class for each configuration entry node.
#
- class Entry
+ class Node
class InvalidError < StandardError; end
attr_reader :config, :metadata
@@ -21,7 +21,7 @@ module Gitlab
end
def [](key)
- @entries[key] || Node::Undefined.new
+ @entries[key] || Entry::Undefined.new
end
def compose!(deps = nil)
diff --git a/lib/gitlab/ci/config/node/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
index 3c6d3a52966..68dad161149 100644
--- a/lib/gitlab/ci/config/node/paths.rb
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents an array of paths.
#
- class Paths < Entry
+ class Paths < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/script.rb b/lib/gitlab/ci/config/entry/script.rb
index 39328f0fade..29ecd9995ca 100644
--- a/lib/gitlab/ci/config/node/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a script.
#
- class Script < Entry
+ class Script < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/services.rb b/lib/gitlab/ci/config/entry/services.rb
index 481e2b66adc..84f8ab780f5 100644
--- a/lib/gitlab/ci/config/node/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a configuration of Docker services.
#
- class Services < Entry
+ class Services < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
index cbc97641f5a..b7afaba1de8 100644
--- a/lib/gitlab/ci/config/node/stage.rb
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a stage for a job.
#
- class Stage < Entry
+ class Stage < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index b1fe45357ff..ec187bd3732 100644
--- a/lib/gitlab/ci/config/node/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a configuration for pipeline stages.
#
- class Stages < Entry
+ class Stages < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index d8b31975088..28b0a9ffe01 100644
--- a/lib/gitlab/ci/config/node/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents a trigger policy for the job.
#
- class Trigger < Entry
+ class Trigger < Node
include Validatable
validations do
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
index 33e78023539..b33b8238230 100644
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ b/lib/gitlab/ci/config/entry/undefined.rb
@@ -1,13 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
- # This class represents an undefined node.
+ # This class represents an undefined entry.
#
- # Implements the Null Object pattern.
- #
- class Undefined < Entry
+ class Undefined < Node
def initialize(*)
super(nil)
end
diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
index a7d1f6131b8..fbb2551e870 100644
--- a/lib/gitlab/ci/config/node/unspecified.rb
+++ b/lib/gitlab/ci/config/entry/unspecified.rb
@@ -1,9 +1,9 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
- # This class represents an unspecified entry node.
+ # This class represents an unspecified entry.
#
# It decorates original entry adding method that indicates it is
# unspecified.
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
index 085e6e988d1..f7f1b111571 100644
--- a/lib/gitlab/ci/config/node/validatable.rb
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -1,13 +1,13 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
module Validatable
extend ActiveSupport::Concern
class_methods do
def validator
- @validator ||= Class.new(Node::Validator).tap do |validator|
+ @validator ||= Class.new(Entry::Validator).tap do |validator|
if defined?(@validations)
@validations.each { |rules| validator.class_eval(&rules) }
end
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
index 43c7e102b50..55343005fe3 100644
--- a/lib/gitlab/ci/config/node/validator.rb
+++ b/lib/gitlab/ci/config/entry/validator.rb
@@ -1,14 +1,14 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
class Validator < SimpleDelegator
include ActiveModel::Validations
- include Node::Validators
+ include Entry::Validators
- def initialize(node)
- super(node)
- @node = node
+ def initialize(entry)
+ super(entry)
+ @entry = entry
end
def messages
@@ -30,7 +30,7 @@ module Gitlab
def key_name
if key.blank?
- @node.class.name.demodulize.underscore.humanize
+ @entry.class.name.demodulize.underscore.humanize
else
key
end
diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index e20908ad3cb..8632dd0e233 100644
--- a/lib/gitlab/ci/config/node/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -1,7 +1,7 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
diff --git a/lib/gitlab/ci/config/node/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index 5f813f81f55..c3b0e651c3a 100644
--- a/lib/gitlab/ci/config/node/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -1,11 +1,11 @@
module Gitlab
module Ci
class Config
- module Node
+ module Entry
##
# Entry that represents environment variables.
#
- class Variables < Entry
+ class Variables < Node
include Validatable
validations do
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index b164f5a2eea..7e3d5647b39 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -1,45 +1,44 @@
module Gitlab
class ContributionsCalendar
- attr_reader :activity_dates, :projects, :user
+ attr_reader :contributor
+ attr_reader :current_user
+ attr_reader :projects
- def initialize(projects, user)
- @projects = projects
- @user = user
+ def initialize(contributor, current_user = nil)
+ @contributor = contributor
+ @current_user = current_user
+ @projects = ContributedProjectsFinder.new(contributor).execute(current_user)
end
def activity_dates
return @activity_dates if @activity_dates.present?
- @activity_dates = {}
+ # Can't use Event.contributions here because we need to check 3 different
+ # project_features for the (currently) 3 different contribution types
date_from = 1.year.ago
+ repo_events = event_counts(date_from, :repository).
+ having(action: Event::PUSHED)
+ issue_events = event_counts(date_from, :issues).
+ having(action: [Event::CREATED, Event::CLOSED], target_type: "Issue")
+ mr_events = event_counts(date_from, :merge_requests).
+ having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
- events = Event.reorder(nil).contributions.where(author_id: user.id).
- where("created_at > ?", date_from).where(project_id: projects).
- group('date(created_at)').
- select('date(created_at) as date, count(id) as total_amount').
- map(&:attributes)
+ union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events])
+ events = Event.find_by_sql(union.to_sql).map(&:attributes)
- activity_dates = (1.year.ago.to_date..Date.today).to_a
-
- activity_dates.each do |date|
- day_events = events.find { |day_events| day_events["date"] == date }
-
- if day_events
- @activity_dates[date] = day_events["total_amount"]
- end
+ @activity_events = events.each_with_object(Hash.new {|h, k| h[k] = 0 }) do |event, activities|
+ activities[event["date"]] += event["total_amount"]
end
-
- @activity_dates
end
def events_by_date(date)
- events = Event.contributions.where(author_id: user.id).
- where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
+ events = Event.contributions.where(author_id: contributor.id).
+ where(created_at: date.beginning_of_day..date.end_of_day).
where(project_id: projects)
- events.select do |event|
- event.push? || event.issue? || event.merge_request?
- end
+ # Use visible_to_user? instead of the complicated logic in activity_dates
+ # because we're only viewing the events for a single day.
+ events.select {|event| event.visible_to_user?(current_user) }
end
def starting_year
@@ -49,5 +48,30 @@ module Gitlab
def starting_month
Date.today.month
end
+
+ private
+
+ def event_counts(date_from, feature)
+ t = Event.arel_table
+
+ # re-running the contributed projects query in each union is expensive, so
+ # use IN(project_ids...) instead. It's the intersection of two users so
+ # the list will be (relatively) short
+ @contributed_project_ids ||= projects.uniq.pluck(:id)
+ authed_projects = Project.where(id: @contributed_project_ids).
+ with_feature_available_for_user(feature, current_user).
+ reorder(nil).
+ select(:id)
+
+ conditions = t[:created_at].gteq(date_from.beginning_of_day).
+ and(t[:created_at].lteq(Date.today.end_of_day)).
+ and(t[:author_id].eq(contributor.id))
+
+ Event.reorder(nil).
+ select(t[:project_id], t[:target_type], t[:action], 'date(created_at) AS date', 'count(id) as total_amount').
+ group(t[:project_id], t[:target_type], t[:action], 'date(created_at)').
+ where(conditions).
+ having(t[:project_id].in(Arel::Nodes::SqlLiteral.new(authed_projects.to_sql)))
+ end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index ef9160d6437..c6bb8f9c8ed 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -23,6 +23,10 @@ module Gitlab
settings || fake_application_settings
end
+ def sidekiq_throttling_enabled?
+ current_application_settings.sidekiq_throttling_enabled?
+ end
+
def fake_application_settings
OpenStruct.new(
default_projects_limit: Settings.gitlab['default_projects_limit'],
@@ -50,6 +54,7 @@ module Gitlab
repository_checks_enabled: true,
container_registry_token_expire_delay: 5,
user_default_external: false,
+ sidekiq_throttling_enabled: false,
)
end
diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event.rb
new file mode 100644
index 00000000000..486139b1687
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/base_event.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module CycleAnalytics
+ class BaseEvent
+ include MetricsTables
+
+ attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
+
+ def initialize(project:, options:)
+ @query = EventsQuery.new(project: project, options: options)
+ @project = project
+ @options = options
+ end
+
+ def fetch
+ update_author!
+
+ event_result.map do |event|
+ serialize(event) if has_permission?(event['id'])
+ end
+ end
+
+ def custom_query(_base_query); end
+
+ def order
+ @order || @start_time_attrs
+ end
+
+ private
+
+ def update_author!
+ return unless event_result.any? && event_result.first['author_id']
+
+ Updater.update!(event_result, from: 'author_id', to: 'author', klass: User)
+ end
+
+ def event_result
+ @event_result ||= @query.execute(self).to_a
+ end
+
+ def serialize(_event)
+ raise NotImplementedError.new("Expected #{self.name} to implement serialize(event)")
+ end
+
+ def has_permission?(id)
+ allowed_ids.nil? || allowed_ids.include?(id.to_i)
+ end
+
+ def allowed_ids
+ nil
+ end
+
+ def event_result_ids
+ event_result.map { |event| event['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/code_event.rb
new file mode 100644
index 00000000000..2afdf0b8518
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/code_event.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module CycleAnalytics
+ class CodeEvent < BaseEvent
+ 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
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb
new file mode 100644
index 00000000000..2d703d76cbb
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events.rb
@@ -0,0 +1,38 @@
+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
new file mode 100644
index 00000000000..2418832ccc2
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/events_query.rb
@@ -0,0 +1,37 @@
+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_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb
new file mode 100644
index 00000000000..a7652a70641
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module IssueAllowed
+ def allowed_ids
+ @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb
new file mode 100644
index 00000000000..705b7e5ce24
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/issue_event.rb
@@ -0,0 +1,27 @@
+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/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
new file mode 100644
index 00000000000..28f6db44759
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/merge_request_allowed.rb
@@ -0,0 +1,9 @@
+module Gitlab
+ module CycleAnalytics
+ module MergeRequestAllowed
+ def allowed_ids
+ @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
new file mode 100644
index 00000000000..b71e8735e27
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_fetcher.rb
@@ -0,0 +1,60 @@
+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/metrics_tables.rb b/lib/gitlab/cycle_analytics/metrics_tables.rb
new file mode 100644
index 00000000000..9d25ef078e8
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/metrics_tables.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module CycleAnalytics
+ module MetricsTables
+ def mr_metrics_table
+ MergeRequest::Metrics.arel_table
+ end
+
+ def mr_table
+ MergeRequest.arel_table
+ end
+
+ def mr_diff_table
+ MergeRequestDiff.arel_table
+ end
+
+ def mr_closing_issues_table
+ MergeRequestsClosingIssues.arel_table
+ end
+
+ def issue_table
+ Issue.arel_table
+ end
+
+ def issue_metrics_table
+ Issue::Metrics.arel_table
+ end
+
+ def user_table
+ User.arel_table
+ end
+
+ def build_table
+ ::CommitStatus.arel_table
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event.rb
new file mode 100644
index 00000000000..b1ae215f348
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/plan_event.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ module CycleAnalytics
+ class PlanEvent < BaseEvent
+ 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)
+ base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
+ end
+
+ private
+
+ def serialize(event)
+ st_commit = first_time_reference_commit(event.delete('commits'), event)
+
+ return unless st_commit
+
+ serialize_commit(event, st_commit, query)
+ end
+
+ def first_time_reference_commit(commits, event)
+ YAML.load(commits).find do |commit|
+ next unless commit[:committed_date] && event['first_mentioned_in_commit_at']
+
+ commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i
+ end
+ end
+
+ def serialize_commit(event, st_commit, query)
+ commit = Commit.new(Gitlab::Git::Commit.new(st_commit), @project)
+
+ AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/production_event.rb
new file mode 100644
index 00000000000..4868c3c6237
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/production_event.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module CycleAnalytics
+ class ProductionEvent < BaseEvent
+ 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],
+ 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/review_event.rb b/lib/gitlab/cycle_analytics/review_event.rb
new file mode 100644
index 00000000000..b394a02cc52
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/review_event.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module CycleAnalytics
+ class ReviewEvent < BaseEvent
+ 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]]
+
+ super(*args)
+ end
+
+ def serialize(event)
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event.rb
new file mode 100644
index 00000000000..a1f30b716f6
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/staging_event.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module CycleAnalytics
+ class StagingEvent < BaseEvent
+ 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]
+
+ super(*args)
+ end
+
+ def fetch
+ Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ super
+ end
+
+ def custom_query(base_query)
+ base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
+ end
+
+ private
+
+ def serialize(event)
+ AnalyticsBuildSerializer.new.represent(event['build']).as_json
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb
new file mode 100644
index 00000000000..d553d0b5aec
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/test_event.rb
@@ -0,0 +1,13 @@
+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/updater.rb b/lib/gitlab/cycle_analytics/updater.rb
new file mode 100644
index 00000000000..953268ebd46
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/updater.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module CycleAnalytics
+ class Updater
+ def self.update!(*args)
+ new(*args).update!
+ end
+
+ def initialize(event_result, from:, to:, klass:)
+ @event_result = event_result
+ @klass = klass
+ @from = from
+ @to = to
+ end
+
+ def update!
+ @event_result.each do |event|
+ event[@to] = items[event.delete(@from).to_i].first
+ end
+ end
+
+ def result_ids
+ @event_result.map { |event| event[@from] }
+ end
+
+ def items
+ @items ||= @klass.find(result_ids).group_by { |item| item['id'] }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 55b8f888d53..2d5c9232425 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -35,6 +35,13 @@ module Gitlab
order
end
+ def self.serialized_transaction
+ opts = {}
+ opts[:isolation] = :serializable unless Rails.env.test? && connection.transaction_open?
+
+ connection.transaction(opts) { yield }
+ end
+
def self.random
Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
end
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
index b6a89f715fd..25e56998038 100644
--- a/lib/gitlab/database/date_time.rb
+++ b/lib/gitlab/database/date_time.rb
@@ -7,21 +7,25 @@ module Gitlab
#
# Note: For MySQL, the interval is returned in seconds.
# For PostgreSQL, the interval is returned as an INTERVAL type.
- def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
- diff_fn = if Gitlab::Database.postgresql?
- Arel::Nodes::Subtraction.new(
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
- elsif Gitlab::Database.mysql?
- Arel::Nodes::NamedFunction.new(
- "TIMESTAMPDIFF",
- [Arel.sql('second'),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
- Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
- end
+ def subtract_datetimes(query_so_far, start_time_attrs, end_time_attrs, as)
+ diff_fn = subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
query_so_far.project(diff_fn.as(as))
end
+
+ def subtract_datetimes_diff(query_so_far, start_time_attrs, end_time_attrs)
+ if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index ce85e5e0123..c6bf25b5874 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -55,6 +55,12 @@ module Gitlab
repository.commit(deleted_file ? old_ref : new_ref)
end
+ def old_content_commit
+ return unless diff_refs
+
+ repository.commit(old_ref)
+ end
+
def old_ref
diff_refs.try(:base_sha)
end
@@ -111,13 +117,10 @@ module Gitlab
diff_lines.count(&:removed?)
end
- def old_blob(commit = content_commit)
+ def old_blob(commit = old_content_commit)
return unless commit
- parent_id = commit.parent_id
- return unless parent_id
-
- repository.blob_at(parent_id, old_path)
+ repository.blob_at(commit.id, old_path)
end
def blob(commit = content_commit)
@@ -126,7 +129,7 @@ module Gitlab
repository.blob_at(commit.id, file_path)
end
- def cache_key
+ def file_identifier
"#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
end
end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index dc4d47c878b..fe7adb7bed6 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -39,7 +39,7 @@ module Gitlab
# hashes that represent serialized diff lines.
#
def cache_highlight!(diff_file)
- item_key = diff_file.cache_key
+ item_key = diff_file.file_identifier
if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index b1a6d5fe0f6..f4d1505ea91 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,39 +2,38 @@
module Gitlab
# Checks if a set of migrations requires downtime or not.
class EeCompatCheck
+ CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+ CHECK_DIR = Rails.root.join('ee_compat_check')
+ MAX_FETCH_DEPTH = 500
+ IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
- attr_reader :ce_branch, :check_dir, :ce_repo
+ attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
- def initialize(branch:, check_dir:, ce_repo: nil)
+ def initialize(branch:, ce_repo: CE_REPO)
+ @repo_dir = CHECK_DIR.join('repo')
+ @patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch
- @check_dir = check_dir
- @ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git'
+ @ce_repo = ce_repo
end
def check
ensure_ee_repo
- delete_patches
+ ensure_patches_dir
generate_patch(ce_branch, ce_patch_full_path)
- Dir.chdir(check_dir) do
- step("In the #{check_dir} directory")
-
- step("Pulling latest master", %w[git pull --ff-only origin master])
+ Dir.chdir(repo_dir) do
+ step("In the #{repo_dir} directory")
status = catch(:halt_check) do
ce_branch_compat_check!
-
- delete_ee_branch_locally
-
+ delete_ee_branch_locally!
ee_branch_presence_check!
-
ee_branch_compat_check!
end
- delete_ee_branch_locally
- delete_patches
+ delete_ee_branch_locally!
if status.nil?
true
@@ -47,20 +46,43 @@ module Gitlab
private
def ensure_ee_repo
- if Dir.exist?(check_dir)
- step("#{check_dir} already exists")
+ if Dir.exist?(repo_dir)
+ step("#{repo_dir} already exists")
else
- cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}]
- step("Cloning #{EE_REPO} into #{check_dir}", cmd)
+ cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
+ step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
end
end
- def ce_branch_compat_check!
- cmd = %W[git apply --check #{ce_patch_full_path}]
- status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd)
+ def ensure_patches_dir
+ FileUtils.mkdir_p(patches_dir)
+ end
+
+ def generate_patch(branch, patch_path)
+ FileUtils.rm(patch_path, force: true)
+
+ depth = 0
+ loop do
+ depth += 50
+ cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
+ Gitlab::Popen.popen(cmd)
+ _, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
+
+ raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
+ break if status.zero?
+ end
- if status.zero?
- puts ce_applies_cleanly_msg(ce_branch)
+ step("Generating the patch against master in #{patch_path}")
+ output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
+ throw(:halt_check, :ko) unless status.zero?
+
+ File.write(patch_path, output)
+ throw(:halt_check, :ko) unless File.exist?(patch_path)
+ end
+
+ def ce_branch_compat_check!
+ if check_patch(ce_patch_full_path).zero?
+ puts applies_cleanly_msg(ce_branch)
throw(:halt_check)
end
end
@@ -80,10 +102,8 @@ module Gitlab
step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
generate_patch(ee_branch, ee_patch_full_path)
- cmd = %W[git apply --check #{ee_patch_full_path}]
- status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd)
- unless status.zero?
+ unless check_patch(ee_patch_full_path).zero?
puts
puts ee_branch_doesnt_apply_cleanly_msg
@@ -91,50 +111,49 @@ module Gitlab
end
puts
- puts ee_applies_cleanly_msg
+ puts applies_cleanly_msg(ee_branch)
end
- def generate_patch(branch, filepath)
- FileUtils.rm(filepath, force: true)
+ def check_patch(patch_path)
+ step("Checking out master", %w[git checkout master])
+ step("Reseting to latest master", %w[git reset --hard origin/master])
- depth = 0
- loop do
- depth += 10
- step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}])
- status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}])
-
- break if status.zero? || depth > 500
- end
+ step("Checking if #{patch_path} applies cleanly to EE/master")
+ output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
- raise "#{branch} is too far behind master, please rebase it!" if depth > 500
+ unless status.zero?
+ failed_files = output.lines.reduce([]) do |memo, line|
+ if line.start_with?('error: patch failed:')
+ file = line.sub(/\Aerror: patch failed: /, '')
+ memo << file unless file =~ IGNORED_FILES_REGEX
+ end
+ memo
+ end
- step("Generating the patch against master")
- output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
- throw(:halt_check, :ko) unless status.zero?
+ if failed_files.empty?
+ status = 0
+ else
+ puts "\nConflicting files:"
+ failed_files.each do |file|
+ puts " - #{file}"
+ end
+ end
+ end
- File.write(filepath, output)
- throw(:halt_check, :ko) unless File.exist?(filepath)
+ status
end
- def delete_ee_branch_locally
+ def delete_ee_branch_locally!
command(%w[git checkout master])
step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
end
- def delete_patches
- step("Deleting #{ce_patch_full_path}")
- FileUtils.rm(ce_patch_full_path, force: true)
-
- step("Deleting #{ee_patch_full_path}")
- FileUtils.rm(ee_patch_full_path, force: true)
- end
-
def ce_patch_name
@ce_patch_name ||= "#{ce_branch}.patch"
end
def ce_patch_full_path
- @ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir)
+ @ce_patch_full_path ||= patches_dir.join(ce_patch_name)
end
def ee_branch
@@ -146,15 +165,18 @@ module Gitlab
end
def ee_patch_full_path
- @ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir)
+ @ee_patch_full_path ||= patches_dir.join(ee_patch_name)
end
def step(desc, cmd = nil)
puts "\n=> #{desc}\n"
if cmd
+ start = Time.now
puts "\n$ #{cmd.join(' ')}"
- command(cmd)
+ status = command(cmd)
+ puts "\nFinished in #{Time.now - start} seconds"
+ status
end
end
@@ -165,12 +187,12 @@ module Gitlab
status
end
- def ce_applies_cleanly_msg(ce_branch)
+ def applies_cleanly_msg(branch)
<<-MSG.strip_heredoc
=================================================================
🎉 Congratulations!! 🎉
- The #{ce_branch} branch applies cleanly to EE/master!
+ The #{branch} branch applies cleanly to EE/master!
Much ❤️!!
=================================================================\n
@@ -211,7 +233,7 @@ module Gitlab
# In the EE repo
$ git fetch origin
- $ git checkout -b #{ee_branch} FETCH_HEAD
+ $ git checkout -b #{ee_branch} origin/master
$ git fetch #{ce_repo} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick
@@ -245,17 +267,5 @@ module Gitlab
=================================================================\n
MSG
end
-
- def ee_applies_cleanly_msg
- <<-MSG.strip_heredoc
- =================================================================
- 🎉 Congratulations!! 🎉
-
- The #{ee_branch} branch applies cleanly to EE/master!
-
- Much ❤️!!
- =================================================================\n
- MSG
- end
end
end
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index 5cf9d5ebe28..bd3267e2a80 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -4,8 +4,7 @@ require 'gitlab/email/handler/create_issue_handler'
module Gitlab
module Email
module Handler
- # The `CreateIssueHandler` feature is disabled for the time being.
- HANDLERS = [CreateNoteHandler]
+ HANDLERS = [CreateNoteHandler, CreateIssueHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 4e6566af8ab..9f90a3ec2b2 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -5,16 +5,16 @@ module Gitlab
module Email
module Handler
class CreateIssueHandler < BaseHandler
- attr_reader :project_path, :authentication_token
+ attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
super(mail, mail_key)
- @project_path, @authentication_token =
+ @project_path, @incoming_email_token =
mail_key && mail_key.split('+', 2)
end
def can_handle?
- !authentication_token.nil?
+ !incoming_email_token.nil?
end
def execute
@@ -29,7 +29,7 @@ module Gitlab
end
def author
- @author ||= User.find_by(authentication_token: authentication_token)
+ @author ||= User.find_by(incoming_email_token: incoming_email_token)
end
def project
diff --git a/lib/gitlab/email/html_parser.rb b/lib/gitlab/email/html_parser.rb
new file mode 100644
index 00000000000..a4ca62bfc41
--- /dev/null
+++ b/lib/gitlab/email/html_parser.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module Email
+ class HTMLParser
+ def self.parse_reply(raw_body)
+ new(raw_body).filtered_text
+ end
+
+ attr_reader :raw_body
+ def initialize(raw_body)
+ @raw_body = raw_body
+ end
+
+ def document
+ @document ||= Nokogiri::HTML.parse(raw_body)
+ end
+
+ def filter_replies!
+ document.xpath('//blockquote').each(&:remove)
+ document.xpath('//table').each(&:remove)
+ end
+
+ def filtered_html
+ @filtered_html ||= begin
+ filter_replies!
+ document.inner_html
+ end
+ end
+
+ def filtered_text
+ @filtered_text ||= Html2Text.convert(filtered_html)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 3411eb1d9ce..85402c2a278 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -23,19 +23,26 @@ module Gitlab
private
def select_body(message)
- text = message.text_part if message.multipart?
- text ||= message if message.content_type !~ /text\/html/
+ if message.multipart?
+ part = message.text_part || message.html_part || message
+ else
+ part = message
+ end
- return "" unless text
+ decoded = fix_charset(part)
- text = fix_charset(text)
+ return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly
- if text =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
+ if decoded =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/
return ""
end
- text
+ if (part.content_type || '').include? 'text/html'
+ HTMLParser.parse_reply(decoded)
+ else
+ decoded
+ end
end
# Force encoding to UTF-8 on a Mail::Message or Mail::Part
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 799794c0171..bcbf6455998 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -2,8 +2,18 @@
# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
+ UnauthorizedError = Class.new(StandardError)
+
+ ERROR_MESSAGES = {
+ upload: 'You are not allowed to upload code for this project.',
+ download: 'You are not allowed to download code from this project.',
+ deploy_key: 'Deploy keys are not allowed to push code.',
+ no_repo: 'A repository for this project does not exist yet.'
+ }
+
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
+ ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
@@ -16,56 +26,43 @@ module Gitlab
end
def check(cmd, changes)
- return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
-
- unless actor
- return build_status_object(false, "No user or key was provided.")
- end
-
- if user && !user_access.allowed?
- return build_status_object(false, "Your account has been blocked.")
- end
-
- unless project && (user_access.can_read_project? || deploy_key_can_read_project?)
- return build_status_object(false, 'The project you were looking for could not be found.')
- end
+ check_protocol!
+ check_active_user!
+ check_project_accessibility!
+ check_command_existence!(cmd)
case cmd
when *DOWNLOAD_COMMANDS
download_access_check
when *PUSH_COMMANDS
push_access_check(changes)
- else
- build_status_object(false, "The command you're trying to execute is not allowed.")
end
+
+ build_status_object(true)
+ rescue UnauthorizedError => ex
+ build_status_object(false, ex.message)
end
def download_access_check
if user
user_download_access_check
- elsif deploy_key
- build_status_object(true)
- else
- raise 'Wrong actor'
+ elsif deploy_key.nil? && !Guest.can?(:download_code, project)
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
end
end
def push_access_check(changes)
if user
user_push_access_check(changes)
- elsif deploy_key
- build_status_object(false, "Deploy keys are not allowed to push code.")
else
- raise 'Wrong actor'
+ raise UnauthorizedError, ERROR_MESSAGES[deploy_key ? :deploy_key : :upload]
end
end
def user_download_access_check
unless user_can_download_code? || build_can_download_code?
- return build_status_object(false, "You are not allowed to download code from this project.")
+ raise UnauthorizedError, ERROR_MESSAGES[:download]
end
-
- build_status_object(true)
end
def user_can_download_code?
@@ -78,15 +75,15 @@ module Gitlab
def user_push_access_check(changes)
unless authentication_abilities.include?(:push_code)
- return build_status_object(false, "You are not allowed to upload code for this project.")
+ raise UnauthorizedError, ERROR_MESSAGES[:upload]
end
if changes.blank?
- return build_status_object(true)
+ return # Allow access.
end
unless project.repository.exists?
- return build_status_object(false, "A repository for this project does not exist yet.")
+ raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end
changes_list = Gitlab::ChangesList.new(changes)
@@ -96,11 +93,9 @@ module Gitlab
status = change_access_check(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
- return status
+ raise UnauthorizedError, status.message
end
end
-
- build_status_object(true)
end
def change_access_check(change)
@@ -113,6 +108,30 @@ module Gitlab
private
+ def check_protocol!
+ unless protocol_allowed?
+ raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
+ end
+ end
+
+ def check_active_user!
+ if user && !user_access.allowed?
+ raise UnauthorizedError, "Your account has been blocked."
+ end
+ end
+
+ def check_project_accessibility!
+ if project.blank? || !can_read_project?
+ raise UnauthorizedError, 'The project you were looking for could not be found.'
+ end
+ end
+
+ def check_command_existence!(cmd)
+ unless ALL_COMMANDS.include?(cmd)
+ raise UnauthorizedError, "The command you're trying to execute is not allowed."
+ end
+ end
+
def matching_merge_request?(newrev, branch_name)
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
@@ -130,6 +149,16 @@ module Gitlab
end
end
+ def can_read_project?
+ if user
+ user_access.can_read_project?
+ elsif deploy_key
+ deploy_key_can_read_project?
+ else
+ Guest.can?(:read_project, project)
+ end
+ end
+
protected
def user
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 90cf38a8513..281b65bdeba 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -20,10 +20,18 @@ module Gitlab
end
def execute
+ # The ordering of importing is important here due to the way GitHub structures their data
+ # 1. Labels are required by other items while not having a dependency on anything else
+ # so need to be first
+ # 2. Pull requests must come before issues. Every pull request is also an issue but not
+ # all issues are pull requests. Only the issue entity has labels defined in GitHub. GitLab
+ # doesn't structure data like this so we need to make sure that we've created the MRs
+ # before we attempt to add the labels defined in the GitHub issue for the related, already
+ # imported, pull request
import_labels
import_milestones
- import_issues
import_pull_requests
+ import_issues
import_comments(:issues)
import_comments(:pull_requests)
import_wiki
@@ -79,13 +87,17 @@ module Gitlab
issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw)
- if gh_issue.valid?
- begin
- issue = gh_issue.create!
- apply_labels(issue, raw)
- rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
- end
+ begin
+ issuable =
+ if gh_issue.pull_request?
+ MergeRequest.find_by_iid(gh_issue.number)
+ else
+ gh_issue.create!
+ end
+
+ apply_labels(issuable, raw)
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end
end
@@ -101,8 +113,7 @@ module Gitlab
restore_source_branch(pull_request) unless pull_request.source_branch_exists?
restore_target_branch(pull_request) unless pull_request.target_branch_exists?
- merge_request = pull_request.create!
- apply_labels(merge_request, raw)
+ pull_request.create!
rescue => e
errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
ensure
@@ -133,21 +144,14 @@ module Gitlab
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
end
- def apply_labels(issuable, raw_issuable)
- # GH returns labels for issues but not for pull requests!
- labels = if issuable.is_a?(MergeRequest)
- client.labels_for_issue(repo, raw_issuable.number)
- else
- raw_issuable.labels
- end
+ def apply_labels(issuable, raw)
+ return unless raw.labels.count > 0
- if labels.count > 0
- label_ids = labels
- .map { |attrs| @labels[attrs.name] }
- .compact
+ label_ids = raw.labels
+ .map { |attrs| @labels[attrs.name] }
+ .compact
- issuable.update_attribute(:label_ids, label_ids)
- end
+ issuable.update_attribute(:label_ids, label_ids)
end
def import_comments(issuable_type)
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 8c32ac59fc5..887690bcc7c 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -32,8 +32,8 @@ module Gitlab
raw_data.number
end
- def valid?
- raw_data.pull_request.nil?
+ def pull_request?
+ raw_data.pull_request.present?
end
private
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index d7be50bd437..801dfde9a36 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,5 +1,7 @@
module Gitlab
module IncomingEmail
+ WILDCARD_PLACEHOLDER = '%{key}'.freeze
+
class << self
FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
@@ -7,8 +9,16 @@ module Gitlab
config.enabled && config.address
end
+ def supports_wildcard?
+ config.address && config.address.include?(WILDCARD_PLACEHOLDER)
+ end
+
+ def supports_issue_creation?
+ enabled? && supports_wildcard?
+ end
+
def reply_address(key)
- config.address.gsub('%{key}', key)
+ config.address.gsub(WILDCARD_PLACEHOLDER, key)
end
def key_from_address(address)
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 8b38cfaefb6..7b05290e5cc 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -89,9 +89,7 @@ module Gitlab
end
def user_filter(filter = nil)
- if config.user_filter.present?
- user_filter = Net::LDAP::Filter.construct(config.user_filter)
- end
+ user_filter = config.constructed_user_filter if config.user_filter.present?
if user_filter && filter
Net::LDAP::Filter.join(filter, user_filter)
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
index bad683c6511..4745311402c 100644
--- a/lib/gitlab/ldap/authentication.rb
+++ b/lib/gitlab/ldap/authentication.rb
@@ -54,11 +54,9 @@ module Gitlab
# Apply LDAP user filter if present
if config.user_filter.present?
- filter = Net::LDAP::Filter.join(
- filter,
- Net::LDAP::Filter.construct(config.user_filter)
- )
+ filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
end
+
filter
end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index f9bb5775323..de52ef3fc65 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def self.providers
- servers.map {|server| server['provider_name'] }
+ servers.map { |server| server['provider_name'] }
end
def self.valid_provider?(provider)
@@ -38,13 +38,31 @@ module Gitlab
end
def adapter_options
- {
- host: options['host'],
- port: options['port'],
- encryption: encryption
- }.tap do |options|
- options.merge!(auth_options) if has_auth?
+ opts = base_options.merge(
+ encryption: encryption,
+ )
+
+ opts.merge!(auth_options) if has_auth?
+
+ opts
+ end
+
+ def omniauth_options
+ opts = base_options.merge(
+ base: base,
+ method: options['method'],
+ filter: omniauth_user_filter,
+ name_proc: name_proc
+ )
+
+ if has_auth?
+ opts.merge!(
+ bind_dn: options['bind_dn'],
+ password: options['password']
+ )
end
+
+ opts
end
def base
@@ -68,6 +86,10 @@ module Gitlab
options['user_filter']
end
+ def constructed_user_filter
+ @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
+ end
+
def group_base
options['group_base']
end
@@ -92,8 +114,31 @@ module Gitlab
options['timeout'].to_i
end
+ def has_auth?
+ options['password'] || options['bind_dn']
+ end
+
+ def allow_username_or_email_login
+ options['allow_username_or_email_login']
+ end
+
+ def name_proc
+ if allow_username_or_email_login
+ Proc.new { |name| name.gsub(/@.*\z/, '') }
+ else
+ Proc.new { |name| name }
+ end
+ end
+
protected
+ def base_options
+ {
+ host: options['host'],
+ port: options['port']
+ }
+ end
+
def base_config
Gitlab.config.ldap
end
@@ -123,8 +168,14 @@ module Gitlab
}
end
- def has_auth?
- options['password'] || options['bind_dn']
+ def omniauth_user_filter
+ uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
+
+ if user_filter.present?
+ Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
+ else
+ uid_filter.to_s
+ end
end
end
end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index a5220d92312..3503fac40e8 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -31,6 +31,7 @@ module Gitlab
config[:ssl] = false if config[:ssl].nil?
config[:start_tls] = false if config[:start_tls].nil?
config[:mailbox] = 'inbox' if config[:mailbox].nil?
+ config[:idle_timeout] = 60 if config[:idle_timeout].nil?
if config[:enabled] && config[:address]
gitlab_redis = Gitlab::Redis.new(rails_env)
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 0a91d3918d5..a8b4dc2a83f 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -102,6 +102,8 @@ module Gitlab
Gitlab::LDAP::Config.providers.each do |provider|
adapter = Gitlab::LDAP::Adapter.new(provider)
@ldap_person = Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter)
+ # The `uid` might actually be a DN. Try it next.
+ @ldap_person ||= Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
break if @ldap_person
end
@ldap_person
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 24733435a5a..66e6b29e798 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -5,11 +5,7 @@ module Gitlab
def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user
@project = project
- @repository_ref = if repository_ref.present?
- repository_ref
- else
- nil
- end
+ @repository_ref = repository_ref.presence || project.default_branch
@query = query
end
@@ -44,36 +40,81 @@ module Gitlab
@commits_count ||= commits.count
end
+ def self.parse_search_result(result)
+ ref = nil
+ filename = nil
+ basename = nil
+ startline = 0
+
+ result.each_line.each_with_index do |line, index|
+ if line =~ /^.*:.*:\d+:/
+ ref, filename, startline = line.split(':')
+ startline = startline.to_i - index
+ extname = Regexp.escape(File.extname(filename))
+ basename = filename.sub(/#{extname}$/, '')
+ break
+ end
+ end
+
+ data = ""
+
+ result.each_line do |line|
+ data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
+ end
+
+ OpenStruct.new(
+ filename: filename,
+ basename: basename,
+ ref: ref,
+ startline: startline,
+ data: data
+ )
+ end
+
private
def blobs
- if project.empty_repo? || query.blank?
- []
- else
- project.repository.search_files(query, repository_ref)
+ @blobs ||= begin
+ blobs = project.repository.search_files_by_content(query, repository_ref).first(100)
+ found_file_names = Set.new
+
+ results = blobs.map do |blob|
+ blob = self.class.parse_search_result(blob)
+ found_file_names << blob.filename
+
+ [blob.filename, blob]
+ end
+
+ project.repository.search_files_by_name(query, repository_ref).first(100).each do |filename|
+ results << [filename, nil] unless found_file_names.include?(filename)
+ end
+
+ results.sort_by(&:first)
end
end
def wiki_blobs
- if project.wiki_enabled? && query.present?
- project_wiki = ProjectWiki.new(project)
+ @wiki_blobs ||= begin
+ if project.wiki_enabled? && query.present?
+ project_wiki = ProjectWiki.new(project)
- unless project_wiki.empty?
- project_wiki.search_files(query)
+ unless project_wiki.empty?
+ project_wiki.search_files(query)
+ else
+ []
+ end
else
[]
end
- else
- []
end
end
def notes
- project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+ @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
end
def commits
- project.repository.find_commits_by_message(query)
+ @commits ||= project.repository.find_commits_by_message(query)
end
def project_ids_relation
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 0d30e1bb92e..47ea8b7e82e 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,12 +2,23 @@ module Gitlab
module Regex
extend self
- NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
+ # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript
+ # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`.
+ # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to
+ # allow non-regex validatiions, etc), `NAMESPACE_REGEX_STR_SIMPLE` serves as a Javascript-compatible version of
+ # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
+ # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
+ NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
+ NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_SIMPLE})(?<!\.git|\.atom)".freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
end
+ def namespace_route_regex
+ @namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze
+ end
+
def namespace_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
"Cannot start with '-' or end in '.', '.git' or '.atom'." \
@@ -22,12 +33,12 @@ module Gitlab
end
def project_name_regex
- @project_name_regex ||= /\A[\p{Alnum}_][\p{Alnum}\p{Pd}_\. ]*\z/.freeze
+ @project_name_regex ||= /\A[\p{Alnum}\u{00A9}-\u{1f9c0}_][\p{Alnum}\p{Pd}\u{00A9}-\u{1f9c0}_\. ]*\z/.freeze
end
def project_name_regex_message
- "can contain only letters, digits, '_', '.', dash and space. " \
- "It must start with letter, digit or '_'."
+ "can contain only letters, digits, emojis, '_', '.', dash, space. " \
+ "It must start with letter, digit, emoji or '_'."
end
def project_path_regex
diff --git a/lib/gitlab/sidekiq_throttler.rb b/lib/gitlab/sidekiq_throttler.rb
new file mode 100644
index 00000000000..d4d39a888e7
--- /dev/null
+++ b/lib/gitlab/sidekiq_throttler.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ class SidekiqThrottler
+ class << self
+ def execute!
+ if Gitlab::CurrentSettings.sidekiq_throttling_enabled?
+ Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_queues.each do |queue|
+ Sidekiq::Queue[queue].limit = queue_limit
+ end
+ end
+ end
+
+ private
+
+ def queue_limit
+ @queue_limit ||=
+ begin
+ factor = Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_factor
+ (factor * Sidekiq.options[:concurrency]).ceil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/mattermost/presenter.rb b/lib/mattermost/presenter.rb
new file mode 100644
index 00000000000..bfbb089eb02
--- /dev/null
+++ b/lib/mattermost/presenter.rb
@@ -0,0 +1,117 @@
+module Mattermost
+ class Presenter
+ class << self
+ include Gitlab::Routing.url_helpers
+
+ def authorize_chat_name(url)
+ message = if url
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(message)
+ end
+
+ def help(commands, trigger)
+ if commands.none?
+ ephemeral_response("No commands configured")
+ else
+ commands.map! { |command| "#{trigger} #{command}" }
+ message = header_with_list("Available commands", commands)
+
+ ephemeral_response(message)
+ end
+ end
+
+ def present(resource)
+ return not_found unless resource
+
+ if resource.respond_to?(:count)
+ if resource.count > 1
+ return multiple_resources(resource)
+ elsif resource.count == 0
+ return not_found
+ else
+ resource = resource.first
+ end
+ end
+
+ single_resource(resource)
+ end
+
+ def access_denied
+ ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+ end
+
+ private
+
+ def not_found
+ ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
+ end
+
+ def single_resource(resource)
+ return error(resource) if resource.errors.any? || !resource.persisted?
+
+ message = "### #{title(resource)}"
+ message << "\n\n#{resource.description}" if resource.description
+
+ in_channel_response(message)
+ end
+
+ def multiple_resources(resources)
+ resources.map! { |resource| title(resource) }
+
+ message = header_with_list("Multiple results were found:", resources)
+
+ ephemeral_response(message)
+ end
+
+ def error(resource)
+ message = header_with_list("The action was not successful, because:", resource.errors.messages)
+
+ ephemeral_response(message)
+ end
+
+ def title(resource)
+ "[#{resource.to_reference} #{resource.title}](#{url(resource)})"
+ end
+
+ def header_with_list(header, items)
+ message = [header]
+
+ items.each do |item|
+ message << "- #{item}"
+ end
+
+ message.join("\n")
+ end
+
+ def url(resource)
+ url_for(
+ [
+ resource.project.namespace.becomes(Namespace),
+ resource.project,
+ resource
+ ]
+ )
+ end
+
+ def ephemeral_response(message)
+ {
+ response_type: :ephemeral,
+ text: message,
+ status: 200
+ }
+ end
+
+ def in_channel_response(message)
+ {
+ response_type: :in_channel,
+ text: message,
+ status: 200
+ }
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 2ae48a970ce..35c4194e87c 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -760,7 +760,7 @@ namespace :gitlab do
end
namespace :ldap do
- task :check, [:limit] => :environment do |t, args|
+ task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100)
@@ -768,7 +768,7 @@ namespace :gitlab do
start_checking "LDAP"
if Gitlab::LDAP::Config.enabled?
- print_users(args.limit)
+ check_ldap(args.limit)
else
puts 'LDAP is disabled in config/gitlab.yml'
end
@@ -776,21 +776,42 @@ namespace :gitlab do
finished_checking "LDAP"
end
- def print_users(limit)
- puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
-
+ def check_ldap(limit)
servers = Gitlab::LDAP::Config.providers
servers.each do |server|
puts "Server: #{server}"
- Gitlab::LDAP::Adapter.open(server) do |adapter|
- users = adapter.users(adapter.config.uid, '*', limit)
- users.each do |user|
- puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+
+ begin
+ Gitlab::LDAP::Adapter.open(server) do |adapter|
+ check_ldap_auth(adapter)
+
+ puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
+
+ users = adapter.users(adapter.config.uid, '*', limit)
+ users.each do |user|
+ puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
+ end
end
+ rescue Net::LDAP::ConnectionRefusedError, Errno::ECONNREFUSED => e
+ puts "Could not connect to the LDAP server: #{e.message}".color(:red)
end
end
end
+
+ def check_ldap_auth(adapter)
+ auth = adapter.config.has_auth?
+
+ if auth && adapter.ldap.bind
+ message = 'Success'.color(:green)
+ elsif auth
+ message = 'Failed. Check `bind_dn` and `password` configuration values'.color(:red)
+ else
+ message = 'Anonymous. No `bind_dn` or `password` configured'.color(:yellow)
+ end
+
+ puts "LDAP authentication... #{message}"
+ end
end
namespace :repo do
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index b7cbdc6cd78..4a696a52b4d 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -91,5 +91,28 @@ namespace :gitlab do
puts "To block these users run this command with BLOCK=true".color(:yellow)
end
end
+
+ # This is a rake task which removes faulty refs. These refs where only
+ # created in the 8.13.RC cycle, and fixed in the stable builds which were
+ # released. So likely this should only be run once on gitlab.com
+ # Faulty refs are moved so they are kept around, else some features break.
+ desc 'GitLab | Cleanup | Remove faulty deployment refs'
+ task move_faulty_deployment_refs: :environment do
+ projects = Project.where(id: Deployment.select(:project_id).distinct)
+
+ projects.find_each do |project|
+ rugged = project.repository.rugged
+
+ max_iid = project.deployments.maximum(:iid)
+
+ rugged.references.each('refs/environments/**/*') do |ref|
+ id = ref.name.split('/').last.to_i
+ next unless id > max_iid
+
+ project.deployments.find(id).create_ref
+ rugged.references.delete(ref)
+ end
+ end
+ end
end
end
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 5ee99dfc810..7db0779def8 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -1,18 +1,19 @@
namespace :gitlab do
namespace :dev do
desc 'Checks if the branch would apply cleanly to EE'
- task ee_compat_check: :environment do
- return if defined?(Gitlab::License)
- return unless ENV['CI']
+ task :ee_compat_check, [:branch] => :environment do |_, args|
+ opts =
+ if ENV['CI']
+ { branch: ENV['CI_BUILD_REF_NAME'] }
+ else
+ unless args[:branch]
+ puts "Must specify a branch as an argument".color(:red)
+ exit 1
+ end
+ args
+ end
- success =
- Gitlab::EeCompatCheck.new(
- branch: ENV['CI_BUILD_REF_NAME'],
- check_dir: File.expand_path('ee-compat-check', __dir__),
- ce_repo: ENV['CI_BUILD_REPO']
- ).check
-
- if success
+ if Gitlab::EeCompatCheck.new(opts || {}).check
exit 0
else
exit 1
diff --git a/package.json b/package.json
index a303c9c1eac..e75e070451b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"eslint-config-airbnb": "^12.0.0",
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.0.1",
+ "eslint-plugin-jasmine": "^1.8.1",
"eslint-plugin-jsx-a11y": "^2.2.3",
"eslint-plugin-react": "^6.4.1"
}
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 1eaafdce389..6e3f76b8399 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/sh
retry() {
if eval "$@"; then
@@ -24,11 +24,12 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
- export FLAGS=(--path vendor --retry 3 --quiet)
+ export FLAGS="--path vendor --retry 3 --quiet"
else
- export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
+ rnd=$(awk 'BEGIN { srand() ; printf("%d\n",rand()*5) }')
+ export PATH="$HOME/bin:/usr/local/bin:/usr/bin:/bin"
cp config/database.yml.mysql config/database.yml
sed "s/username\:.*$/username\: runner/" -i config/database.yml
sed "s/password\:.*$/password\: 'password'/" -i config/database.yml
- sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml
+ sed "s/gitlabhq_test/gitlabhq_test_$rnd/" -i config/database.yml
fi
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 22bf3055538..294fae95752 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -47,6 +47,7 @@ describe 'mail_room.yml' do
expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com')
expect(mailbox[:password]).to eq('[REDACTED]')
expect(mailbox[:name]).to eq('inbox')
+ expect(mailbox[:idle_timeout]).to eq(60)
redis_url = gitlab_redis.url
sentinels = gitlab_redis.sentinels
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c7db84dd5f9..60db0192dfd 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
new file mode 100644
index 00000000000..899d8ebd12b
--- /dev/null
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Groups::LabelsController do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+
+ sign_in(user)
+ end
+
+ describe 'POST #toggle_subscription' do
+ it 'allows user to toggle subscription on group labels' do
+ label = create(:group_label, group: group)
+
+ post :toggle_subscription, group_id: group.to_param, id: label.to_param
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 33c75e7584f..6fc6ea95e13 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -7,6 +7,40 @@ describe HelpController do
sign_in(user)
end
+ describe 'GET #index' do
+ context 'when url prefixed without /help/' do
+ it 'has correct url prefix' do
+ stub_readme("[API](api/README.md)")
+ get :index
+ expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
+ end
+ end
+
+ context 'when url prefixed with help/' do
+ it 'will be an absolute path' do
+ stub_readme("[API](help/api/README.md)")
+ get :index
+ expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
+ end
+ end
+
+ context 'when url prefixed with help' do
+ it 'will be an absolute path' do
+ stub_readme("[API](helpful_hints/README.md)")
+ get :index
+ expect(assigns[:help_index]).to eq '[API](/help/helpful_hints/README.md)'
+ end
+ end
+
+ context 'when url prefixed with /help/' do
+ it 'will not be changed' do
+ stub_readme("[API](/help/api/README.md)")
+ get :index
+ expect(assigns[:help_index]).to eq '[API](/help/api/README.md)'
+ end
+ end
+ end
+
describe 'GET #show' do
context 'for Markdown formats' do
context 'when requested file exists' do
@@ -72,4 +106,8 @@ describe HelpController do
end
end
end
+
+ def stub_readme(content)
+ allow(File).to receive(:read).and_return(content)
+ end
end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index cbe0417a4a7..299d2c981d3 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -25,7 +25,7 @@ describe Projects::Boards::IssuesController do
create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
- issue.subscribe(johndoe)
+ issue.subscribe(johndoe, project)
list_issues user: user, board: board, list: list2
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 644de308c64..f7cf006efd6 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe Projects::BranchesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:developer) { create(:user) }
before do
- sign_in(user)
-
project.team << [user, :master]
+ project.team << [user, :developer]
allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz'])
allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0'])
@@ -19,6 +19,8 @@ describe Projects::BranchesController do
context "on creation of a new branch" do
before do
+ sign_in(user)
+
post :create,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -68,6 +70,10 @@ describe Projects::BranchesController do
let(:branch) { "1-feature-branch" }
let!(:issue) { create(:issue, project: project) }
+ before do
+ sign_in(user)
+ end
+
it 'redirects' do
post :create,
namespace_id: project.namespace.to_param,
@@ -94,6 +100,10 @@ describe Projects::BranchesController do
describe "POST destroy with HTML format" do
render_views
+ before do
+ sign_in(user)
+ end
+
it 'returns 303' do
post :destroy,
format: :html,
@@ -109,6 +119,8 @@ describe Projects::BranchesController do
render_views
before do
+ sign_in(user)
+
post :destroy,
format: :js,
id: branch,
@@ -139,4 +151,42 @@ describe Projects::BranchesController do
it { expect(response).to have_http_status(404) }
end
end
+
+ describe "DELETE destroy_all_merged" do
+ def destroy_all_merged
+ delete :destroy_all_merged,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when user is allowed to push' do
+ before do
+ sign_in(user)
+ end
+
+ it 'redirects to branches' do
+ destroy_all_merged
+
+ expect(response).to redirect_to namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'starts worker to delete merged branches' do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:async_execute)
+
+ destroy_all_merged
+ end
+ end
+
+ context 'when user is not allowed to push' do
+ before do
+ sign_in(developer)
+ end
+
+ it 'responds with status 404' do
+ destroy_all_merged
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 768105cae95..bc5e2711125 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::EnvironmentsController do
+ include ApiHelpers
+
let(:environment) { create(:environment) }
let(:project) { environment.project }
let(:user) { create(:user) }
@@ -11,6 +13,27 @@ describe Projects::EnvironmentsController do
sign_in(user)
end
+ describe 'GET index' do
+ context 'when standardrequest has been made' do
+ it 'responds with status code 200' do
+ get :index, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'when requesting JSON response' do
+ it 'responds with correct JSON' do
+ get :index, environment_params(format: :json)
+
+ first_environment = json_response.first
+
+ expect(first_environment).not_to be_empty
+ expect(first_environment['name']). to eq environment.name
+ end
+ end
+ end
+
describe 'GET show' do
context 'with valid id' do
it 'responds with a status code 200' do
@@ -48,11 +71,9 @@ describe Projects::EnvironmentsController do
end
end
- def environment_params
- {
- namespace_id: project.namespace,
- project_id: project,
- id: environment.id
- }
+ def environment_params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace,
+ project_id: project,
+ id: environment.id)
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index ac3469cb8a9..028ea067a97 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -67,4 +67,62 @@ describe Projects::ForksController do
end
end
end
+
+ describe 'GET new' do
+ def get_new
+ get :new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+
+ context 'when user is signed in' do
+ it 'responds with status 200' do
+ sign_in(user)
+
+ get_new
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects to the sign-in page' do
+ sign_out(user)
+
+ get_new
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ describe 'POST create' do
+ def post_create
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ namespace_key: user.namespace.id
+ end
+
+ context 'when user is signed in' do
+ it 'responds with status 302' do
+ sign_in(user)
+
+ post_create
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
+ end
+ end
+
+ context 'when user is not signed in' do
+ it 'redirects to the sign-in page' do
+ sign_out(user)
+
+ post_create
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 8faecec0063..ec6cea5c0f4 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -72,14 +72,8 @@ describe Projects::LabelsController do
end
describe 'POST #generate' do
- let(:admin) { create(:admin) }
-
- before do
- sign_in(admin)
- end
-
context 'personal project' do
- let(:personal_project) { create(:empty_project) }
+ let(:personal_project) { create(:empty_project, namespace: user.namespace) }
it 'creates labels' do
post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
@@ -96,4 +90,26 @@ describe Projects::LabelsController do
end
end
end
+
+ describe 'POST #toggle_subscription' do
+ it 'allows user to toggle subscription on project labels' do
+ label = create(:label, project: project)
+
+ toggle_subscription(label)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'allows user to toggle subscription on group labels' do
+ group_label = create(:group_label, group: group)
+
+ toggle_subscription(group_label)
+
+ expect(response).to have_http_status(200)
+ end
+
+ def toggle_subscription(label)
+ post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 49127aecc63..1d0750d1719 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -39,6 +39,17 @@ describe Projects::MergeRequestsController do
end
end
+ shared_examples "loads labels" do |action|
+ it "loads labels into the @labels variable" do
+ get action,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: 'html'
+ expect(assigns(:labels)).not_to be_nil
+ end
+ end
+
describe "GET show" do
shared_examples "export merge as" do |format|
it "does generally work" do
@@ -51,6 +62,8 @@ describe Projects::MergeRequestsController do
expect(response).to be_success
end
+ it_behaves_like "loads labels", :show
+
it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
@@ -406,6 +419,8 @@ describe Projects::MergeRequestsController do
get :diffs, params.merge(extra_params)
end
+ it_behaves_like "loads labels", :diffs
+
context 'with default params' do
context 'as html' do
before { go(format: 'html') }
@@ -612,6 +627,8 @@ describe Projects::MergeRequestsController do
format: format
end
+ it_behaves_like "loads labels", :commits
+
context 'as html' do
it 'renders the show template' do
go
@@ -630,6 +647,14 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET builds' do
+ it_behaves_like "loads labels", :builds
+ end
+
+ describe 'GET pipelines' do
+ it_behaves_like "loads labels", :pipelines
+ end
+
describe 'GET conflicts' do
let(:json_response) { JSON.parse(response.body) }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 2a7523c6512..b52137fbe7e 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -2,7 +2,7 @@ require('spec_helper')
describe Projects::ProjectMembersController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 8eefa284ba0..5ddcaa60dc6 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -7,6 +7,26 @@ describe ProjectsController do
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ describe 'GET index' do
+ context 'as a user' do
+ it 'redirects to root page' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context 'as a guest' do
+ it 'redirects to Explore page' do
+ get :index
+
+ expect(response).to redirect_to(explore_root_path)
+ end
+ end
+ end
+
describe "GET show" do
context "user not project member" do
before { sign_in(user) }
@@ -264,6 +284,33 @@ describe ProjectsController do
end
end
+ describe 'PUT #new_issue_address' do
+ subject do
+ put :new_issue_address,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param
+ user.reload
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ end
+
+ it 'has http status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'changes the user incoming email token' do
+ expect { subject }.to change { user.incoming_email_token }
+ end
+
+ it 'changes projects new issue address' do
+ expect { subject }.to change { project.new_issue_address(user) }
+ end
+ end
+
describe "POST #toggle_star" do
it "toggles star if user is signed in" do
sign_in(user)
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 191e290a118..954fc2eaf21 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -3,11 +3,11 @@ require 'rails_helper'
describe SentNotificationsController, type: :controller do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
- let(:sent_notification) { create(:sent_notification, noteable: issue, recipient: user) }
+ let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
let(:issue) do
create(:issue, project: project, author: user) do |issue|
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
end
end
@@ -17,7 +17,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
it 'unsubscribes the user' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
@@ -33,7 +33,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key) }
it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'does not set the flash message' do
@@ -53,7 +53,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key.reverse) }
it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'does not set the flash message' do
@@ -69,7 +69,7 @@ describe SentNotificationsController, type: :controller do
before { get(:unsubscribe, id: sent_notification.reply_key, force: true) }
it 'unsubscribes the user' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
@@ -85,14 +85,14 @@ describe SentNotificationsController, type: :controller do
context 'when the force param is not passed' do
let(:merge_request) do
create(:merge_request, source_project: project, author: user) do |merge_request|
- merge_request.subscriptions.create(user: user, subscribed: true)
+ merge_request.subscriptions.create(user: user, project: project, subscribed: true)
end
end
- let(:sent_notification) { create(:sent_notification, noteable: merge_request, recipient: user) }
+ let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) }
before { get(:unsubscribe, id: sent_notification.reply_key) }
it 'unsubscribes the user' do
- expect(merge_request.subscribed?(user)).to be_falsey
+ expect(merge_request.subscribed?(user, project)).to be_falsey
end
it 'sets the flash message' do
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
new file mode 100644
index 00000000000..24225468d55
--- /dev/null
+++ b/spec/factories/chat_names.rb
@@ -0,0 +1,16 @@
+FactoryGirl.define do
+ factory :chat_name, class: ChatName do
+ user factory: :user
+ service factory: :service
+
+ team_id 'T0001'
+ team_domain 'Awesome Team'
+
+ sequence :chat_id do |n|
+ "U#{n}"
+ end
+ sequence :chat_name do |n|
+ "user#{n}"
+ end
+ end
+end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 6f24bf58d14..29ad1af9fd9 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,8 +3,9 @@ FactoryGirl.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
+ user
project nil
-
+ deployable factory: :ci_build
environment factory: :environment
after(:build) do |deployment, evaluator|
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 846cccfc7fa..0852dda6b29 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -4,5 +4,33 @@ FactoryGirl.define do
project factory: :empty_project
sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" }
+
+ trait :with_review_app do |environment|
+ project
+
+ transient do
+ ref 'master'
+ end
+
+ # At this point `review app` is an ephemeral concept related to
+ # deployments being deployed for given environment. There is no
+ # first-class `review app` available so we need to create set of
+ # interconnected objects to simulate a review app.
+ #
+ after(:create) do |environment, evaluator|
+ deployment = create(:deployment,
+ environment: environment,
+ project: environment.project,
+ ref: evaluator.ref,
+ sha: environment.project.commit(evaluator.ref).id)
+
+ teardown_build = create(:ci_build, :manual,
+ name: "#{deployment.environment.name}:teardown",
+ pipeline: deployment.deployable.pipeline)
+
+ deployment.update_column(:on_stop, teardown_build.name)
+ environment.update_attribute(:deployments, [deployment])
+ end
+ end
end
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 2d47a6f6c4c..ebd3595ea64 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -15,5 +15,9 @@ FactoryGirl.define do
trait :private do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+
+ trait :access_requestable do
+ request_access_enabled true
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index dd4a86b1e31..1166498ddff 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -24,6 +24,10 @@ FactoryGirl.define do
visibility_level Gitlab::VisibilityLevel::PRIVATE
end
+ trait :access_requestable do
+ request_access_enabled true
+ end
+
trait :empty_repo do
after(:create) do |project|
project.create_repository
@@ -49,13 +53,17 @@ FactoryGirl.define do
end
after(:create) do |project, evaluator|
+ # Builds and MRs can't have higher visibility level than repository access level.
+ builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
+ merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
+
project.project_feature.
- update_attributes(
+ update_attributes!(
wiki_access_level: evaluator.wiki_access_level,
- builds_access_level: evaluator.builds_access_level,
+ builds_access_level: builds_access_level,
snippets_access_level: evaluator.snippets_access_level,
issues_access_level: evaluator.issues_access_level,
- merge_requests_access_level: evaluator.merge_requests_access_level,
+ merge_requests_access_level: merge_requests_access_level,
repository_access_level: evaluator.repository_access_level
)
end
diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb
new file mode 100644
index 00000000000..b11b0a0a17b
--- /dev/null
+++ b/spec/factories/subscriptions.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :subscription do
+ user
+ project factory: :empty_project
+ subscribable factory: :issue
+ end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
new file mode 100644
index 00000000000..f6d625fa7f6
--- /dev/null
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+feature 'Admin Groups', feature: true do
+ let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
+
+ before do
+ login_as(:admin)
+
+ stub_application_setting(default_group_visibility: internal)
+ end
+
+ describe 'create a group' do
+ scenario 'shows the visibility level radio populated with the default value' do
+ visit new_admin_group_path
+
+ expect_selected_visibility(internal)
+ end
+ end
+
+ describe 'group edit' do
+ scenario 'shows the visibility level radio populated with the group visibility_level value' do
+ group = create(:group, :private)
+
+ visit edit_admin_group_path(group)
+
+ expect_selected_visibility(group.visibility_level)
+ end
+ end
+
+ def expect_selected_visibility(level)
+ selector = "#group_visibility_level_#{level}[checked=checked]"
+
+ expect(page).to have_selector(selector, count: 1)
+ end
+end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 6cb8753e8fc..4aa84fb65d9 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -659,6 +659,10 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
end
+ it 'displays lists' do
+ expect(page).to have_selector('.board')
+ end
+
it 'does not show create new list' do
expect(page).not_to have_selector('.js-new-board-list')
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 760a8967123..a03cd6fbf2d 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -46,7 +46,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
click_button 'Cancel'
- expect(page).to have_selector('.board-new-issue-form', visible: false)
+ expect(page).not_to have_selector('.board-new-issue-form')
end
end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
new file mode 100644
index 00000000000..41dcfe439c2
--- /dev/null
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Navigation bar counter', feature: true, js: true, caching: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ issue.update(assignee: user)
+ merge_request.update(assignee: user)
+ login_as(user)
+ end
+
+ it 'reflects dashboard issues count' do
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+
+ issue.update(assignee: nil)
+ visit issues_dashboard_path
+
+ expect_counters('issues', '1')
+ end
+
+ it 'reflects dashboard merge requests count' do
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+
+ merge_request.update(assignee: nil)
+ visit merge_requests_dashboard_path
+
+ expect_counters('merge_requests', '1')
+ end
+
+ def expect_counters(issuable_type, count)
+ dashboard_count = find('li.active span.badge')
+ nav_count = find(".dashboard-shortcuts-#{issuable_type} span.count")
+
+ expect(nav_count).to have_content(count)
+ expect(dashboard_count).to have_content(count)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
new file mode 100644
index 00000000000..0c1939fd885
--- /dev/null
+++ b/spec/features/environment_spec.rb
@@ -0,0 +1,161 @@
+require 'spec_helper'
+
+feature 'Environment', :feature do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ feature 'environment details page' do
+ given!(:environment) { create(:environment, project: project) }
+ given!(:deployment) { }
+ given!(:manual) { }
+
+ before do
+ visit_environment(environment)
+ end
+
+ context 'without deployments' do
+ scenario 'does show no deployments' do
+ expect(page).to have_content('You don\'t have any deployments right now.')
+ end
+ end
+
+ context 'with deployments' do
+ context 'when there is no related deployable' do
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: nil)
+ end
+
+ scenario 'does show deployment SHA' do
+ expect(page).to have_link(deployment.short_sha)
+ end
+
+ scenario 'does not show a re-deploy button for deployment without build' do
+ expect(page).not_to have_link('Re-deploy')
+ end
+ end
+
+ context 'with related deployable present' do
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ scenario 'does show build name' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ scenario 'does show re-deploy button' do
+ expect(page).to have_link('Re-deploy')
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+
+ context 'with manual action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+
+ scenario 'does show a play button' do
+ expect(page).to have_link(manual.name.humanize)
+ end
+
+ scenario 'does allow to play manual action' do
+ expect(manual).to be_skipped
+ expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
+ expect(page).to have_content(manual.name)
+ expect(manual.reload).to be_pending
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
+
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+
+ scenario 'does show stop button' do
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
+
+ expect(page).to have_content('close_app')
+ end
+
+ context 'for reporter' do
+ let(:role) { :reporter }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ feature 'auto-close environment when branch is deleted' do
+ given(:project) { create(:project) }
+
+ given!(:environment) do
+ create(:environment, :with_review_app, project: project,
+ ref: 'feature')
+ end
+
+ scenario 'user visits environment page' do
+ visit_environment(environment)
+
+ expect(page).to have_link('Stop')
+ end
+
+ scenario 'user deletes the branch with running environment' do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ remove_branch_with_hooks(project, user, 'feature') do
+ page.within('.js-branch-feature') { find('a.btn-remove').click }
+ end
+
+ visit_environment(environment)
+
+ expect(page).to have_no_link('Stop')
+ end
+
+ ##
+ # This is a workaround for problem described in #24543
+ #
+ def remove_branch_with_hooks(project, user, branch)
+ params = {
+ oldrev: project.commit(branch).id,
+ newrev: Gitlab::Git::BLANK_SHA,
+ ref: "refs/heads/#{branch}"
+ }
+
+ yield
+
+ GitPushService.new(project, user, params).execute
+ end
+ end
+
+ def visit_environment(environment)
+ visit namespace_project_environment_path(environment.project.namespace,
+ environment.project,
+ environment)
+ end
+end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index b565586ee14..c7fe622c477 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -1,228 +1,147 @@
require 'spec_helper'
-feature 'Environments', feature: true do
+feature 'Environments page', :feature, :js do
given(:project) { create(:empty_project) }
given(:user) { create(:user) }
given(:role) { :developer }
background do
- login_as(user)
project.team << [user, role]
+ login_as(user)
end
- describe 'when showing environments' do
- given!(:environment) { }
- given!(:deployment) { }
- given!(:manual) { }
-
- before do
- visit namespace_project_environments_path(project.namespace, project)
- end
+ given!(:environment) { }
+ given!(:deployment) { }
+ given!(:manual) { }
- context 'shows two tabs' do
- scenario 'shows "Available" and "Stopped" tab with links' do
- expect(page).to have_link('Available')
- expect(page).to have_link('Stopped')
- end
- end
-
- context 'without environments' do
- scenario 'does show no environments' do
- expect(page).to have_content('You don\'t have any environments right now.')
- end
+ before do
+ visit_environments(project)
+ end
- scenario 'does show 0 as counter for environments in both tabs' do
- expect(page.find('.js-available-environments-count').text).to eq('0')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
+ describe 'page tabs' do
+ scenario 'shows "Available" and "Stopped" tab with links' do
+ expect(page).to have_link('Available')
+ expect(page).to have_link('Stopped')
end
+ end
- context 'with environments' do
- given(:environment) { create(:environment, project: project) }
-
- scenario 'does show environment name' do
- expect(page).to have_link(environment.name)
- end
-
- scenario 'does show number of available and stopped environments' do
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
- end
-
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('No deployments yet')
- end
- end
-
- context 'with deployments' do
- given(:deployment) { create(:deployment, environment: environment) }
-
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
-
- scenario 'does show deployment internal id' do
- expect(page).to have_content(deployment.iid)
- end
-
- context 'with build and manual actions' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
-
- scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
- end
-
- scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
- end
-
- scenario 'does show build name and id' do
- expect(page).to have_link("#{build.name} (##{build.id})")
- end
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
-
- scenario 'does not show external link button' do
- expect(page).not_to have_css('external-url')
- end
-
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
-
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
- end
-
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
-
- scenario 'does show stop button' do
- expect(page).to have_selector('.stop-env-link')
- end
-
- scenario 'starts build when stop button clicked' do
- first('.stop-env-link').click
-
- expect(page).to have_content('close_app')
- end
-
- context 'for reporter' do
- let(:role) { :reporter }
-
- scenario 'does not show stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
- end
- end
- end
- end
+ context 'without environments' do
+ scenario 'does show no environments' do
+ expect(page).to have_content('You don\'t have any environments right now.')
end
- scenario 'does have a New environment button' do
- expect(page).to have_link('New environment')
+ scenario 'does show 0 as counter for environments in both tabs' do
+ expect(page.find('.js-available-environments-count').text).to eq('0')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
end
describe 'when showing the environment' do
given(:environment) { create(:environment, project: project) }
- given!(:deployment) { }
- given!(:manual) { }
- before do
- visit namespace_project_environment_path(project.namespace, project, environment)
+ scenario 'does show environment name' do
+ expect(page).to have_link(environment.name)
+ end
+
+ scenario 'does show number of available and stopped environments' do
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
context 'without deployments' do
scenario 'does show no deployments' do
- expect(page).to have_content('You don\'t have any deployments right now.')
+ expect(page).to have_content('No deployments yet')
end
end
context 'with deployments' do
- given(:deployment) { create(:deployment, environment: environment) }
+ given(:project) { create(:project) }
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ sha: project.commit.id)
+ end
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
- scenario 'does not show a re-deploy button for deployment without build' do
- expect(page).not_to have_link('Re-deploy')
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
end
- context 'with build' do
+ context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- scenario 'does show build name' do
- expect(page).to have_link("#{build.name} (##{build.id})")
+ given(:manual) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
- scenario 'does show re-deploy button' do
- expect(page).to have_link('Re-deploy')
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ sha: project.commit.id)
end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ scenario 'does show a play button' do
+ find('.dropdown-play-icon-container').click
+ expect(page).to have_content(manual.name.humanize)
end
- context 'with manual action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+ scenario 'does allow to play manual action', js: true do
+ expect(manual).to be_skipped
- scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
- end
+ find('.dropdown-play-icon-container').click
+ expect(page).to have_content(manual.name.humanize)
- scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
- end
+ expect { click_link(manual.name.humanize) }
+ .not_to change { Ci::Pipeline.count }
- context 'with external_url' do
- given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
- given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+ expect(manual.reload).to be_pending
+ end
- scenario 'does show an external link button' do
- expect(page).to have_link(nil, href: environment.external_url)
- end
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} ##{build.id}")
+ end
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+
+ scenario 'does not show external link button' do
+ expect(page).not_to have_css('external-url')
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
end
+ end
- context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ context 'with stop action' do
+ given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
+ scenario 'does show stop button' do
+ expect(page).to have_selector('.stop-env-link')
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ scenario 'starts build when stop button clicked' do
+ find('.stop-env-link').click
- expect(page).to have_content('close_app')
- end
+ expect(page).to have_content('close_app')
+ end
- context 'for reporter' do
- let(:role) { :reporter }
+ context 'for reporter' do
+ let(:role) { :reporter }
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
+ scenario 'does not show stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
end
end
end
@@ -230,9 +149,13 @@ feature 'Environments', feature: true do
end
end
+ scenario 'does have a New environment button' do
+ expect(page).to have_link('New environment')
+ end
+
describe 'when creating a new environment' do
before do
- visit namespace_project_environments_path(project.namespace, project)
+ visit_environments(project)
end
context 'when logged as developer' do
@@ -271,4 +194,8 @@ feature 'Environments', feature: true do
end
end
end
+
+ def visit_environments(project)
+ visit namespace_project_environments_path(project.namespace, project)
+ end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 6c938bdead8..3934c936f20 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -182,6 +182,20 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
end
end
+
+ context 'expanding a diff when symlink was converted to a regular file' do
+ let(:branch) { 'symlink-expand-diff' }
+
+ it 'shows the content of the regular file' do
+ expect(page).to have_content('This diff is collapsed')
+ expect(page).to have_no_content('No longer a symlink')
+
+ find('.click-to-expand').click
+ wait_for_ajax
+
+ expect(page).to have_content('No longer a symlink')
+ end
+ end
end
context 'visiting a commit without collapsed diffs' do
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
new file mode 100644
index 00000000000..f6409e00f22
--- /dev/null
+++ b/spec/features/global_search_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Global search', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ end
+
+ describe 'I search through the issues and I see pagination' do
+ before do
+ allow_any_instance_of(Gitlab::SearchResults).to receive(:per_page).and_return(1)
+ create_list(:issue, 2, project: project, title: 'initial')
+ end
+
+ it "has a pagination" do
+ visit dashboard_projects_path
+
+ fill_in "search", with: "initial"
+ click_button "Go"
+
+ select_filter("Issues")
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ end
+ end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
new file mode 100644
index 00000000000..476eca17a9d
--- /dev/null
+++ b/spec/features/groups/issues_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Group issues page', feature: true do
+ let(:path) { issues_group_path(group) }
+ let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
+
+ include_examples 'project features apply to issuables', Issue
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index d811b05b0c3..dbe150823ba 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > Owner manages access requests', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
background do
group.request_access(user)
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index b3baa2ab57c..d8c9c487996 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Groups > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let!(:project) { create(:project, :private, namespace: group) }
background do
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
new file mode 100644
index 00000000000..a2791b57544
--- /dev/null
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+feature 'Group merge requests page', feature: true do
+ let(:path) { merge_requests_group_path(group) }
+ let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: "this is my created issuable")}
+
+ include_examples 'project features apply to issuables', MergeRequest
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 13bfe90302c..4b19886274e 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -80,7 +80,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description > p > strong')
+ expect(page).to have_css('.group-home-desc > p > strong')
end
it 'passes through html-pipeline' do
@@ -88,7 +88,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description > p > img')
+ expect(page).to have_css('.group-home-desc > p > img')
end
it 'sanitizes unwanted tags' do
@@ -96,7 +96,7 @@ feature 'Group', feature: true do
visit path
- expect(page).not_to have_css('.description h1')
+ expect(page).not_to have_css('.group-home-desc h1')
end
it 'permits `rel` attribute on links' do
@@ -104,7 +104,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.description a[rel]')
+ expect(page).to have_css('.group-home-desc a[rel]')
end
end
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 4b1aec8bf71..bc068b5e7e0 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -1,7 +1,9 @@
require 'rails_helper'
feature 'Issue Sidebar', feature: true do
- let(:project) { create(:project) }
+ include WaitForAjax
+
+ let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -10,6 +12,37 @@ feature 'Issue Sidebar', feature: true do
login_as(user)
end
+ context 'assignee', js: true do
+ let(:user2) { create(:user) }
+ let(:issue2) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user, :developer]
+ visit_issue(project, issue2)
+
+ find('.block.assignee .edit-link').click
+
+ wait_for_ajax
+ end
+
+ it 'shows author in assignee dropdown' do
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content(user2.name)
+ end
+ end
+
+ it 'shows author when filtering assignee dropdown' do
+ page.within '.dropdown-menu-user' do
+ find('.dropdown-input-field').native.send_keys user2.name
+ sleep 1 # Required to wait for end of input delay
+
+ wait_for_ajax
+
+ expect(page).to have_content(user2.name)
+ end
+ end
+ end
+
context 'as a allowed user' do
before do
project.team << [user, :developer]
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index fb0c4704285..ab901e74617 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -18,22 +18,24 @@ feature 'Start new branch from an issue', feature: true do
end
context "when there is a referenced merge request" do
- let(:note) do
- create(:note, :on_issue, :system, project: project,
+ let!(:note) do
+ create(:note, :on_issue, :system, project: project, noteable: issue,
note: "Mentioned in !#{referenced_mr.iid}")
end
+
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
description: "Fixes ##{issue.iid}", author: user)
end
before do
- issue.notes << note
+ referenced_mr.cache_merge_request_closes_issues!(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it "hides the new branch button", js: true do
+ expect(page).to have_css('#new-branch .unavailable')
expect(page).not_to have_css('#new-branch .available')
expect(page).to have_content /1 Related Merge Request/
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index b504329656f..cdd02a8c8e3 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'Issues', feature: true do
include IssueHelpers
include SortingHelper
+ include WaitForAjax
let(:project) { create(:project) }
@@ -368,6 +369,26 @@ describe 'Issues', feature: true do
end
end
+ describe 'when I want to reset my incoming email token' do
+ let(:project1) { create(:project, namespace: @user.namespace) }
+
+ before do
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ project1.team << [@user, :master]
+ visit namespace_project_issues_path(@user.namespace, project1)
+ end
+
+ it 'changes incoming email address token', js: true do
+ find('.issue-email-modal-btn').click
+ previous_token = find('input#issue_email').value
+
+ find('.incoming-email-token-reset').click
+ wait_for_ajax
+
+ expect(find('input#issue_email').value).not_to eq(previous_token)
+ end
+ end
+
describe 'update labels from issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
let!(:label) { create(:label, project: project) }
@@ -553,7 +574,7 @@ describe 'Issues', feature: true do
end
end
- xdescribe 'new issue by email' do
+ describe 'new issue by email' do
shared_examples 'show the email in the modal' do
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index c68e1ea4af9..702869b6e8b 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -67,4 +67,21 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Source branch "non-exist-source" does not exist')
expect(page).to have_content('Target branch "non-exist-target" does not exist')
end
+
+ context 'when a branch contains commits that both delete and add the same image' do
+ it 'renders the diff successfully' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
+
+ click_link "Changes"
+
+ expect(page).to have_content "6049019_460s.jpg"
+ end
+ end
+
+ # Isolates a regression (see #24627)
+ it 'does not show error messages on initial form' do
+ visit new_namespace_project_merge_request_path(project.namespace, project)
+ expect(page).not_to have_selector('#error_explanation')
+ expect(page).not_to have_content('The form contains the following error')
+ end
end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 5e6d8467217..eab64bd4b54 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -69,8 +69,6 @@ feature 'Diff notes resolve', feature: true, js: true do
page.within '.diff-content .note' do
expect(page).to have_selector('.line-resolve-btn.is-active')
-
- expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
page.within '.line-resolve-all-container' do
@@ -203,7 +201,7 @@ feature 'Diff notes resolve', feature: true, js: true do
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
- expect(page).to have_content('Last updated')
+ expect(page).not_to have_content('Last updated')
page.within '.line-resolve-all-container' do
expect(page).to have_content('0/1 discussion resolved')
diff --git a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
index c3c3ab33872..8eceaad2457 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -73,7 +73,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
expect(page).to have_button "Merge When Build Succeeds"
visit_merge_request(merge_request) # refresh the page
- expect(page).to have_content "Canceled the automatic merge"
+ expect(page).to have_content "canceled the automatic merge"
end
it "allows the user to remove the source branch" do
@@ -101,7 +101,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
expect(page).not_to have_link "Merge When Build Succeeds"
end
end
-
+
def visit_merge_request(merge_request)
visit namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c3d8c349ca4..7a562b5e03d 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -32,4 +32,33 @@ describe 'Profile account page', feature: true do
expect(current_path).to eq(profile_account_path)
end
end
+
+ describe 'when I reset private token' do
+ before do
+ visit profile_account_path
+ end
+
+ it 'resets private token' do
+ previous_token = find("#private-token").value
+
+ click_link('Reset private token')
+
+ expect(find('#private-token').value).not_to eq(previous_token)
+ end
+ end
+
+ describe 'when I reset incoming email token' do
+ before do
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ visit profile_account_path
+ end
+
+ it 'resets incoming email token' do
+ previous_token = find('#incoming-email-token').value
+
+ click_link('Reset incoming email token')
+
+ expect(find('#incoming-email-token').value).not_to eq(previous_token)
+ end
+ end
end
diff --git a/spec/features/profiles/chat_names_spec.rb b/spec/features/profiles/chat_names_spec.rb
new file mode 100644
index 00000000000..6f6f7029c0b
--- /dev/null
+++ b/spec/features/profiles/chat_names_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+feature 'Profile > Chat', feature: true do
+ given(:user) { create(:user) }
+ given(:service) { create(:service) }
+
+ before do
+ login_as(user)
+ end
+
+ describe 'uses authorization link' do
+ given(:params) do
+ { team_id: 'T00', team_domain: 'my_chat_team', user_id: 'U01', user_name: 'my_chat_user' }
+ end
+ given!(:authorize_url) { ChatNames::AuthorizeUserService.new(service, params).execute }
+ given(:authorize_path) { URI.parse(authorize_url).request_uri }
+
+ before do
+ visit authorize_path
+ end
+
+ context 'clicks authorize' do
+ before do
+ click_button 'Authorize'
+ end
+
+ scenario 'goes to list of chat names and see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).to have_content('my_chat_team')
+ expect(page).to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+
+ context 'clicks deny' do
+ before do
+ click_button 'Deny'
+ end
+
+ scenario 'goes to list of chat names and do not see chat account' do
+ expect(page.current_path).to eq(profile_chat_names_path)
+ expect(page).not_to have_content('my_chat_team')
+ expect(page).not_to have_content('my_chat_user')
+ end
+
+ scenario 'second use of link is denied' do
+ visit authorize_path
+
+ expect(page).to have_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'visits chat accounts' do
+ given!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ before do
+ visit profile_chat_names_path
+ end
+
+ scenario 'sees chat user' do
+ expect(page).to have_content(chat_name.team_domain)
+ expect(page).to have_content(chat_name.chat_name)
+ end
+
+ scenario 'removes chat account' do
+ click_link 'Remove'
+
+ expect(page).to have_content("You don't have any active chat names.")
+ end
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index e45e3a36d01..d46d9e9399e 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit from a different branch", js: true do
it do
- find('.commit-action-buttons a.dropdown-toggle').click
+ find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
new file mode 100644
index 00000000000..69295e450d0
--- /dev/null
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'user checks git blame', feature: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_tree_path(project.namespace, project, project.default_branch)
+ end
+
+ scenario "can see blame of '.gitignore'" do
+ click_link ".gitignore"
+ click_link 'Blame'
+
+ expect(page).to have_content "*.rb"
+ expect(page).to have_content "Dmitriy Zaporozhets"
+ expect(page).to have_content "Initial commit"
+ end
+end
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
new file mode 100644
index 00000000000..3130d87fba5
--- /dev/null
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+feature 'Labels subscription', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature) { create(:group_label, group: group, title: 'feature') }
+
+ context 'when signed in' do
+ before do
+ project.team << [user, :developer]
+ login_as user
+ end
+
+ scenario 'users can subscribe/unsubscribe to labels', js: true do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content('bug')
+ expect(page).to have_content('feature')
+
+ within "#project_label_#{bug.id}" do
+ expect(page).not_to have_button 'Unsubscribe'
+
+ click_button 'Subscribe'
+
+ expect(page).not_to have_button 'Subscribe'
+ expect(page).to have_button 'Unsubscribe'
+
+ click_button 'Unsubscribe'
+
+ expect(page).to have_button 'Subscribe'
+ expect(page).not_to have_button 'Unsubscribe'
+ end
+
+ within "#group_label_#{feature.id}" do
+ expect(page).not_to have_button 'Unsubscribe'
+
+ click_link_on_dropdown('Group level')
+
+ expect(page).not_to have_selector('.dropdown-group-label')
+ expect(page).to have_button 'Unsubscribe'
+
+ click_button 'Unsubscribe'
+
+ expect(page).to have_selector('.dropdown-group-label')
+
+ click_link_on_dropdown('Project level')
+
+ expect(page).not_to have_selector('.dropdown-group-label')
+ expect(page).to have_button 'Unsubscribe'
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'users can not subscribe/unsubscribe to labels' do
+ visit namespace_project_labels_path(project.namespace, project)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'feature'
+ expect(page).not_to have_button('Subscribe')
+ expect(page).not_to have_selector('.dropdown-group-label')
+ end
+ end
+
+ def click_link_on_dropdown(text)
+ find('.dropdown-group-label').click
+
+ page.within('.dropdown-group-label') do
+ find('a.js-subscribe-button', text: text).click
+ end
+ end
+end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
index c4ed92d2780..4973e0aee85 100644
--- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
feature 'Projects > Members > Group requester cannot request access to project', feature: true do
let(:user) { create(:user) }
let(:owner) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:group) { create(:group, :public, :access_requestable) }
+ let(:project) { create(:project, :public, :access_requestable, namespace: group) }
background do
group.add_owner(owner)
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index d15376931c3..143390b71cd 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > Master manages access requests', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
background do
project.request_access(user)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 56ede8eb5be..97c42bd7f01 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
feature 'Projects > Members > User requests access', feature: true do
let(:user) { create(:user) }
let(:master) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :access_requestable) }
background do
project.team << [master, :master]
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
new file mode 100644
index 00000000000..abfc46601fb
--- /dev/null
+++ b/spec/features/projects/new_project_spec.rb
@@ -0,0 +1,19 @@
+require "spec_helper"
+
+feature "New project", feature: true do
+ context "Visibility level selector" do
+ let(:user) { create(:admin) }
+
+ before { login_as(user) }
+
+ Gitlab::VisibilityLevel.options.each do |key, level|
+ it "sets selector to #{key}" do
+ stub_application_setting(default_project_visibility: level)
+
+ visit new_project_path
+
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
index db56a50e058..002c6f6b359 100644
--- a/spec/features/projects/pipelines_spec.rb
+++ b/spec/features/projects/pipelines_spec.rb
@@ -146,7 +146,8 @@ describe "Pipelines" do
end
describe 'GET /:project/pipelines/:id' do
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master') }
+ 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')
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 3de25d7af7d..bf60cca4ea4 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -18,7 +18,7 @@ describe 'Edit Project Settings', feature: true do
click_button 'Save changes'
expect(page).to have_field 'project_name_edit', with: 'foo&bar'
- expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ 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
end
@@ -34,8 +34,21 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_field 'Project name', with: 'foo&bar'
expect(page).to have_field 'Path', with: 'foo&bar'
- expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'."
+ 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_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end
end
+
+ describe 'Rename repository name with emojis' do
+ it 'shows error for invalid project name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'Project name', with: '🚀 foo bar ☁️'
+
+ click_button 'Rename project'
+
+ expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
+ expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
+ end
+ end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 1806200c82c..caecd027aaa 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -100,6 +100,32 @@ describe "Search", feature: true do
expect(page).to have_link(snippet.title)
end
+
+ it 'finds a commit' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: 'add'
+ click_button 'Go'
+ end
+
+ click_link "Commits"
+
+ expect(page).to have_selector('.commit-row-description')
+ end
+
+ it 'finds a code' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: 'def'
+ click_button 'Go'
+ end
+
+ click_link "Code"
+
+ expect(page).to have_selector('.file-content .code')
+ end
end
describe 'Right header search field', feature: true do
diff --git a/spec/features/snippets/explore_spec.rb b/spec/features/snippets/explore_spec.rb
new file mode 100644
index 00000000000..10a4597e467
--- /dev/null
+++ b/spec/features/snippets/explore_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+feature 'Explore Snippets', feature: true do
+ scenario 'User should see snippets that are not private' do
+ public_snippet = create(:personal_snippet, :public)
+ internal_snippet = create(:personal_snippet, :internal)
+ private_snippet = create(:personal_snippet, :private)
+
+ login_as create(:user)
+ visit explore_snippets_path
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+end
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
new file mode 100644
index 00000000000..146cd3af848
--- /dev/null
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+feature 'Search Snippets', feature: true do
+ scenario 'User searches for snippets by title' do
+ public_snippet = create(:personal_snippet, :public, title: 'Beginning and Middle')
+ private_snippet = create(:personal_snippet, :private, title: 'Middle and End')
+
+ login_as private_snippet.author
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'Middle'
+ click_button 'Go'
+ end
+
+ click_link 'Titles and Filenames'
+
+ expect(page).to have_link(public_snippet.title)
+ expect(page).to have_link(private_snippet.title)
+ end
+
+ scenario 'User searches for snippet contents' do
+ create(:personal_snippet,
+ :public,
+ title: 'Many lined snippet',
+ content: <<-CONTENT.strip_heredoc
+ |line one
+ |line two
+ |line three
+ |line four
+ |line five
+ |line six
+ |line seven
+ |line eight
+ |line nine
+ |line ten
+ |line eleven
+ |line twelve
+ |line thirteen
+ |line fourteen
+ CONTENT
+ )
+
+ login_as create(:user)
+ visit dashboard_snippets_path
+
+ page.within '.search' do
+ fill_in 'search', with: 'line seven'
+ click_button 'Go'
+ end
+
+ expect(page).to have_content('line seven')
+
+ # 3 lines before the matched line should be visible
+ expect(page).to have_content('line six')
+ expect(page).to have_content('line five')
+ expect(page).to have_content('line four')
+ expect(page).not_to have_content('line three')
+
+ # 3 lines after the matched line should be visible
+ expect(page).to have_content('line eight')
+ expect(page).to have_content('line nine')
+ expect(page).to have_content('line ten')
+ expect(page).not_to have_content('line eleven')
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3ae83ac082d..88eabea7e3a 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
@@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("Good job! Looks like you don't have any todos left.")
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
end
@@ -152,7 +152,7 @@ describe 'Dashboard Todos', feature: true do
within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
- expect(page).to have_content "Good job! Looks like you don't have any todos left."
+ expect(page).to have_selector('.todos-all-done', count: 1)
end
end
end
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index 33b52d1547e..e2d9cfdd0b0 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -26,11 +26,11 @@ describe 'Unsubscribe links', feature: true do
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe'
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
expect(current_path).to eq new_user_session_path
end
@@ -38,11 +38,11 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Cancel'
- expect(issue.subscribed?(recipient)).to be_truthy
+ expect(issue.subscribed?(recipient, project)).to be_truthy
expect(current_path).to eq new_user_session_path
end
end
@@ -51,7 +51,7 @@ describe 'Unsubscribe links', feature: true do
visit header_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
end
@@ -62,14 +62,14 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
it 'unsubscribes from the issue when visiting the link from the header' do
visit header_link
expect(page).to have_text('unsubscribed')
- expect(issue.subscribed?(recipient)).to be_falsey
+ expect(issue.subscribed?(recipient, project)).to be_falsey
end
end
end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 111ca7f7a70..afa98f3f715 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -74,16 +74,29 @@ feature 'Users', feature: true, js: true do
visit new_user_session_path
click_link 'Register'
end
+
+ scenario 'doesn\'t show an error border if the username is available' do
+ fill_in username_input, with: 'new-user'
+ wait_for_ajax
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
+ scenario 'does not show an error border if the username contains dots (.)' do
+ fill_in username_input, with: 'new.user.username'
+ wait_for_ajax
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
scenario 'shows an error border if the username already exists' do
fill_in username_input, with: user.username
wait_for_ajax
expect(find('.username')).to have_css '.gl-field-error-outline'
end
- scenario 'doesn\'t show an error border if the username is available' do
- fill_in username_input, with: 'new-user'
+ scenario 'shows an error border if the username contains special characters' do
+ fill_in username_input, with: 'new$user!username'
wait_for_ajax
- expect(find('#new_user_username')).not_to have_css '.gl-field-error-outline'
+ expect(find('.username')).to have_css '.gl-field-error-outline'
end
end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
index 8cfea9659cb..c7278e971ae 100644
--- a/spec/finders/access_requests_finder_spec.rb
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -3,12 +3,17 @@ require 'spec_helper'
describe AccessRequestsFinder, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- before do
- project.request_access(access_requester)
- group.request_access(access_requester)
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.request_access(access_requester)
+ end
+ end
+
+ let(:group) do
+ create(:group, :public, :access_requestable) do |group|
+ group.request_access(access_requester)
+ end
end
shared_examples 'a finder returning access requesters' do |method_name|
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index fbe09b28b3c..00eec3f3f4c 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -38,7 +38,10 @@ describe GroupProjectsFinder do
end
describe 'without group member current_user' do
- before { shared_project_2.team << [current_user, Gitlab::Access::MASTER] }
+ before do
+ shared_project_2.team << [current_user, Gitlab::Access::MASTER]
+ current_user.reload
+ end
context "only shared" do
context "without external user" do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 10cfb66ec1c..9085cc8debf 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -64,6 +64,21 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1]
end
+
+ context 'as an administrator' do
+ it 'does not return labels from another project' do
+ # Purposefully creating a project with _nothing_ associated to it
+ isolated_project = create(:empty_project)
+ admin = create(:admin)
+
+ # project_3 has a label associated to it, which we don't want coming
+ # back when we ask for the isolated project's labels
+ project_3.team << [admin, :reporter]
+ finder = described_class.new(admin, project_id: isolated_project.id)
+
+ expect(finder.execute).to be_empty
+ end
+ end
end
context 'filtering by title' do
diff --git a/spec/fixtures/emails/outlook_html.eml b/spec/fixtures/emails/outlook_html.eml
new file mode 100644
index 00000000000..506d69efe83
--- /dev/null
+++ b/spec/fixtures/emails/outlook_html.eml
@@ -0,0 +1,140 @@
+
+MIME-Version: 1.0
+Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT)
+X-Originating-IP: [117.207.85.84]
+In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+References: <topic/35@discourse.techapj.com>
+ <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail>
+Date: Wed, 8 Oct 2014 10:47:17 +0530
+Delivered-To: arpit@techapj.com
+Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com>
+Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse!
+From: Arpit Jalan <arpit@techapj.com>
+To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US
+Content-Language: en-US
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+x-originating-ip: [134.68.31.227]
+Content-Type: multipart/alternative;
+ boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_"
+MIME-Version: 1.0
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_
+Content-Type: text/html; charset="utf-8"
+Content-Transfer-Encoding: base64
+
+PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVy
+bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgeG1sbnM6dz0idXJuOnNjaGVt
+YXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6d29yZCIgeG1sbnM6bT0iaHR0cDovL3NjaGVtYXMubWlj
+cm9zb2Z0LmNvbS9vZmZpY2UvMjAwNC8xMi9vbW1sIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv
+VFIvUkVDLWh0bWw0MCI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIg
+Y29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRv
+ciIgY29udGVudD0iTWljcm9zb2Z0IFdvcmQgMTQgKGZpbHRlcmVkIG1lZGl1bSkiPg0KPCEtLVtp
+ZiAhbXNvXT48c3R5bGU+dlw6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kb1w6KiB7
+YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kd1w6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0
+I1ZNTCk7fQ0KLnNoYXBlIHtiZWhhdmlvcjp1cmwoI2RlZmF1bHQjVk1MKTt9DQo8L3N0eWxlPjwh
+W2VuZGlmXS0tPjxzdHlsZT48IS0tDQovKiBGb250IERlZmluaXRpb25zICovDQpAZm9udC1mYWNl
+DQoJe2ZvbnQtZmFtaWx5OkNhbGlicmk7DQoJcGFub3NlLTE6MiAxNSA1IDIgMiAyIDQgMyAyIDQ7
+fQ0KQGZvbnQtZmFjZQ0KCXtmb250LWZhbWlseTpUYWhvbWE7DQoJcGFub3NlLTE6MiAxMSA2IDQg
+MyA1IDQgNCAyIDQ7fQ0KLyogU3R5bGUgRGVmaW5pdGlvbnMgKi8NCnAuTXNvTm9ybWFsLCBsaS5N
+c29Ob3JtYWwsIGRpdi5Nc29Ob3JtYWwNCgl7bWFyZ2luOjBpbjsNCgltYXJnaW4tYm90dG9tOi4w
+MDAxcHQ7DQoJZm9udC1zaXplOjEyLjBwdDsNCglmb250LWZhbWlseToiVGltZXMgTmV3IFJvbWFu
+Iiwic2VyaWYiO30NCmE6bGluaywgc3Bhbi5Nc29IeXBlcmxpbmsNCgl7bXNvLXN0eWxlLXByaW9y
+aXR5Ojk5Ow0KCWNvbG9yOmJsdWU7DQoJdGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZTt9DQphOnZp
+c2l0ZWQsIHNwYW4uTXNvSHlwZXJsaW5rRm9sbG93ZWQNCgl7bXNvLXN0eWxlLXByaW9yaXR5Ojk5
+Ow0KCWNvbG9yOnB1cnBsZTsNCgl0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lO30NCnANCgl7bXNv
+LXN0eWxlLXByaW9yaXR5Ojk5Ow0KCW1zby1tYXJnaW4tdG9wLWFsdDphdXRvOw0KCW1hcmdpbi1y
+aWdodDowaW47DQoJbXNvLW1hcmdpbi1ib3R0b20tYWx0OmF1dG87DQoJbWFyZ2luLWxlZnQ6MGlu
+Ow0KCWZvbnQtc2l6ZToxMi4wcHQ7DQoJZm9udC1mYW1pbHk6IlRpbWVzIE5ldyBSb21hbiIsInNl
+cmlmIjt9DQpzcGFuLkVtYWlsU3R5bGUxOA0KCXttc28tc3R5bGUtdHlwZTpwZXJzb25hbC1yZXBs
+eTsNCglmb250LWZhbWlseToiQ2FsaWJyaSIsInNhbnMtc2VyaWYiOw0KCWNvbG9yOiMxRjQ5N0Q7
+fQ0KLk1zb0NocERlZmF1bHQNCgl7bXNvLXN0eWxlLXR5cGU6ZXhwb3J0LW9ubHk7DQoJZm9udC1m
+YW1pbHk6IkNhbGlicmkiLCJzYW5zLXNlcmlmIjt9DQpAcGFnZSBXb3JkU2VjdGlvbjENCgl7c2l6
+ZTo4LjVpbiAxMS4waW47DQoJbWFyZ2luOjEuMGluIDEuMGluIDEuMGluIDEuMGluO30NCmRpdi5X
+b3JkU2VjdGlvbjENCgl7cGFnZTpXb3JkU2VjdGlvbjE7fQ0KLS0+PC9zdHlsZT48IS0tW2lmIGd0
+ZSBtc28gOV0+PHhtbD4NCjxvOnNoYXBlZGVmYXVsdHMgdjpleHQ9ImVkaXQiIHNwaWRtYXg9IjEw
+MjYiIC8+DQo8L3htbD48IVtlbmRpZl0tLT48IS0tW2lmIGd0ZSBtc28gOV0+PHhtbD4NCjxvOnNo
+YXBlbGF5b3V0IHY6ZXh0PSJlZGl0Ij4NCjxvOmlkbWFwIHY6ZXh0PSJlZGl0IiBkYXRhPSIxIiAv
+Pg0KPC9vOnNoYXBlbGF5b3V0PjwveG1sPjwhW2VuZGlmXS0tPg0KPC9oZWFkPg0KPGJvZHkgbGFu
+Zz0iRU4tVVMiIGxpbms9ImJsdWUiIHZsaW5rPSJwdXJwbGUiPg0KPGRpdiBjbGFzcz0iV29yZFNl
+Y3Rpb24xIj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTEu
+MHB0O2ZvbnQtZmFtaWx5OiZxdW90O0NhbGlicmkmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90
+Oztjb2xvcjojMUY0OTdEIj5NaWNyb3NvZnQgT3V0bG9vayAyMDEwPG86cD48L286cD48L3NwYW4+
+PC9wPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMS4wcHQ7
+Zm9udC1mYW1pbHk6JnF1b3Q7Q2FsaWJyaSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7O2Nv
+bG9yOiMxRjQ5N0QiPjxvOnA+Jm5ic3A7PC9vOnA+PC9zcGFuPjwvcD4NCjxwIGNsYXNzPSJNc29O
+b3JtYWwiPjxiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTAuMHB0O2ZvbnQtZmFtaWx5OiZxdW90
+O1RhaG9tYSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7Ij5Gcm9tOjwvc3Bhbj48L2I+PHNw
+YW4gc3R5bGU9ImZvbnQtc2l6ZToxMC4wcHQ7Zm9udC1mYW1pbHk6JnF1b3Q7VGFob21hJnF1b3Q7
+LCZxdW90O3NhbnMtc2VyaWYmcXVvdDsiPiBtaWNoYWVsIFttYWlsdG86dGFsa0BvcGVubXJzLm9y
+Z10NCjxicj4NCjxiPlNlbnQ6PC9iPiBNb25kYXksIE9jdG9iZXIgMTMsIDIwMTQgOTozOCBBTTxi
+cj4NCjxiPlRvOjwvYj4gUG93ZXIsIENocmlzPGJyPg0KPGI+U3ViamVjdDo8L2I+IFtQTV0gWW91
+ciBwb3N0IGluICZxdW90O0J1cmdlcmhhdXM6IE5ldyByZXN0YXVyYW50IC8gbHVuY2ggdmVudWUm
+cXVvdDs8bzpwPjwvbzpwPjwvc3Bhbj48L3A+DQo8cCBjbGFzcz0iTXNvTm9ybWFsIj48bzpwPiZu
+YnNwOzwvbzpwPjwvcD4NCjxkaXY+DQo8dGFibGUgY2xhc3M9Ik1zb05vcm1hbFRhYmxlIiBib3Jk
+ZXI9IjAiIGNlbGxzcGFjaW5nPSIwIiBjZWxscGFkZGluZz0iMCI+DQo8dGJvZHk+DQo8dHI+DQo8
+dGQgdmFsaWduPSJ0b3AiIHN0eWxlPSJwYWRkaW5nOjBpbiAwaW4gMGluIDBpbiI+PC90ZD4NCjx0
+ZCBzdHlsZT0icGFkZGluZzowaW4gMGluIDBpbiAwaW4iPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCIg
+c3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMu
+b3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWJHbFph
+MVYwZVhoQ1kwMU1SVEZzVURKbVl6VlFNMFpsZWpFNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lq
+b3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNS
+aGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRYTmxjbk5jWEZ3dmJXbGphR0ZsYkZ3aUxGd2lhV1Jj
+SWpwY0ltUTFZbU13TjJOa05EUmpaRFE0TUdNNFlUZzJNemxqWldJMU56Z3pZbVkyWENJc1hDSjFj
+bXhmYVdSelhDSTZXMXdpWWpoa09EZzFNams1TnpkbVpqWTFaV1l5TlRFM09XUmlOR1l5TVdJM056
+RmpOemhqWmpoa09Gd2lYWDBpZlEiIHRhcmdldD0iX2JsYW5rIj48Yj48c3BhbiBzdHlsZT0iZm9u
+dC1zaXplOjEwLjBwdDtmb250LWZhbWlseTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1z
+ZXJpZiZxdW90Oztjb2xvcjojMDA2Njk5O3RleHQtZGVjb3JhdGlvbjpub25lIj5taWNoYWVsPC9z
+cGFuPjwvYj48L2E+PGJyPg0KPHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4LjVwdDtmb250LWZhbWls
+eTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90Oztjb2xvcjojOTk5OTk5
+Ij5PY3RvYmVyIDEzPC9zcGFuPg0KPG86cD48L286cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4N
+Cjx0ZCBjb2xzcGFuPSIyIiBzdHlsZT0icGFkZGluZzozLjc1cHQgMGluIDBpbiAwaW4iPg0KPHAg
+Y2xhc3M9Ik1zb05vcm1hbCIgc3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0i
+aHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5v
+cmc/cD1leUp6SWpvaVVFUklTVU55UjNsVk1EZEJWVmhwV25SM1dXeDRNV05zVFc1Wklpd2lkaUk2
+TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9k
+SFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRGeGNYQzlpZFhKblpY
+Sm9ZWFZ6TFc1bGR5MXlaWE4wWVhWeVlXNTBMV3gxYm1Ob0xYWmxiblZsWEZ4Y0x6WTNNbHhjWEM4
+elhDSXNYQ0pwWkZ3aU9sd2laRFZpWXpBM1kyUTBOR05rTkRnd1l6aGhPRFl6T1dObFlqVTNPRE5p
+WmpaY0lpeGNJblZ5YkY5cFpITmNJanBiWENKaU56WmlZamswWlRGaU56STVaVGsyWlRSbFpXTTRO
+R1JtTWpRNE1ETXdZall5WVdZeU1HTTBYQ0pkZlNKOSI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMw
+MDY2OTk7dGV4dC1kZWNvcmF0aW9uOm5vbmUiPmh0dHBzOi8vdGFsay5vcGVubXJzLm9yZy90L2J1
+cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjcyLzM8L3NwYW4+PC9iPjwvYT4N
+CjxvOnA+PC9vOnA+PC9wPg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6MGluIj5Mb29rcyBsaWtlIHlv
+dXIgcmVwbHktYnktZW1haWwgd2Fzbid0IHByb2Nlc3NlZCBjb3JyZWN0bHkgYnkgb3VyIHNvZnR3
+YXJlLiBDYW4geW91IGxldCBtZSBrbm93IHdoYXQgdmVyc2lvbi9PUyBvZiB3aGF0IGVtYWlsIHBy
+b2dyYW0geW91J3JlIHVzaW5nPyBXZSB3aWxsIHdhbnQgdG8gdHJ5IHRvIGZpeCB0aGUgYnVnLiA6
+c21pbGU6PG86cD48L286cD48L3A+DQo8cCBzdHlsZT0ibWFyZ2luLXRvcDowaW4iPlRoYW5rcyE8
+bzpwPjwvbzpwPjwvcD4NCjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8ZGl2IGNs
+YXNzPSJNc29Ob3JtYWwiIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJ0ZXh0LWFsaWduOmNlbnRlciI+
+DQo8aHIgc2l6ZT0iMSIgd2lkdGg9IjEwMCUiIGFsaWduPSJjZW50ZXIiPg0KPC9kaXY+DQo8ZGl2
+Pg0KPHA+PHNwYW4gc3R5bGU9ImNvbG9yOiM2NjY2NjYiPlRvIHJlc3BvbmQsIHJlcGx5IHRvIHRo
+aXMgZW1haWwgb3IgdmlzaXQgPGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2Ns
+aWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWVYaDJWbnBGTUhSMU1uRm5a
+RWR1TlhFd01GcFFPVlp0VFZvNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05T
+eGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0x
+eWN5NXZjbWRjWEZ3dmRGeGNYQzk1YjNWeUxYQnZjM1F0YVc0dFluVnlaMlZ5YUdGMWN5MXVaWGN0
+Y21WemRHRjFjbUZ1ZEMxc2RXNWphQzEyWlc1MVpWeGNYQzgyTnpSY1hGd3ZNVndpTEZ3aWFXUmNJ
+anBjSW1RMVltTXdOMk5rTkRSalpEUTRNR000WVRnMk16bGpaV0kxTnpnelltWTJYQ0lzWENKMWNt
+eGZhV1J6WENJNlcxd2lZamMyWW1JNU5HVXhZamN5T1dVNU5tVTBaV1ZqT0RSa1pqSTBPREF6TUdJ
+Mk1tRm1NakJqTkZ3aVhYMGlmUSI+DQo8Yj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjY5OTt0ZXh0
+LWRlY29yYXRpb246bm9uZSI+aHR0cHM6Ly90YWxrLm9wZW5tcnMub3JnL3QveW91ci1wb3N0LWlu
+LWJ1cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjc0LzE8L3NwYW4+PC9iPjwv
+YT4gaW4geW91ciBicm93c2VyLjxvOnA+PC9vOnA+PC9zcGFuPjwvcD4NCjwvZGl2Pg0KPGRpdj4N
+CjxwPjxzcGFuIHN0eWxlPSJjb2xvcjojNjY2NjY2Ij5UbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNl
+IGVtYWlscywgdmlzaXQgeW91ciA8YSBocmVmPSJodHRwOi8vY2wub3Blbm1ycy5vcmcvdHJhY2sv
+Y2xpY2svMzAwMzk5MDUvdGFsay5vcGVubXJzLm9yZz9wPWV5SnpJam9pZFV4dVdsZzVWRmMwT1da
+V1MwWTRiRmRMZG1seVdHc3hUVjl6SWl3aWRpSTZNU3dpY0NJNkludGNJblZjSWpvek1EQXpPVGt3
+TlN4Y0luWmNJam94TEZ3aWRYSnNYQ0k2WENKb2RIUndjenBjWEZ3dlhGeGNMM1JoYkdzdWIzQmxi
+bTF5Y3k1dmNtZGNYRnd2YlhsY1hGd3ZjSEpsWm1WeVpXNWpaWE5jSWl4Y0ltbGtYQ0k2WENKa05X
+SmpNRGRqWkRRMFkyUTBPREJqT0dFNE5qTTVZMlZpTlRjNE0ySm1ObHdpTEZ3aWRYSnNYMmxrYzF3
+aU9sdGNJbUk0TVdVd1pqQTFORFk1TkRNME56Z3lNMkZtTWpBMk5qRmpaamMzWkdOaU4yTmhZemRt
+TWpKY0lsMTlJbjAiPg0KPGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMwMDY2OTk7dGV4dC1kZWNvcmF0
+aW9uOm5vbmUiPnVzZXIgcHJlZmVyZW5jZXM8L3NwYW4+PC9iPjwvYT4uPG86cD48L286cD48L3Nw
+YW4+PC9wPg0KPC9kaXY+DQo8L2Rpdj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxpbWcgYm9yZGVy
+PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBpZD0iX3gwMDAwX2kxMDI2IiBzcmM9Imh0dHA6Ly9j
+bC5vcGVubXJzLm9yZy90cmFjay9vcGVuLnBocD91PTMwMDM5OTA1JmFtcDtpZD1kNWJjMDdjZDQ0
+Y2Q0ODBjOGE4NjM5Y2ViNTc4M2JmNiI+PG86cD48L286cD48L3A+DQo8L2Rpdj4NCjwvYm9keT4N
+CjwvaHRtbD4NCg==
+
+--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_--
diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_incoming_email_token.eml
index 0994c2f7775..0994c2f7775 100644
--- a/spec/fixtures/emails/wrong_authentication_token.eml
+++ b/spec/fixtures/emails/wrong_incoming_email_token.eml
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 73f5470cf35..15863d444f8 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -57,7 +57,7 @@ describe ApplicationHelper do
it 'returns an url for the avatar' do
project = create(:project, avatar: File.open(avatar_file_path))
- avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
+ avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
@@ -67,7 +67,7 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
- avatar_url = 'http://localhost' + namespace_project_avatar_path(project.namespace, project)
+ avatar_url = "http://#{Gitlab.config.gitlab.host}#{namespace_project_avatar_path(project.namespace, project)}"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).to match(
image_tag(avatar_url))
end
@@ -218,42 +218,24 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
- expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
+ expect(element.attr('class')).to eq 'js-timeago'
end
it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')).
- to eq 'js-timeago custom_class js-timeago-pending'
+ to eq 'js-timeago custom_class'
end
it 'accepts a custom tooltip placement' do
expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
end
- it 're-initializes timeago Javascript' do
- el = element.next_element
-
- expect(el.name).to eq 'script'
- expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
- end
-
- it 'allows the script tag to be excluded' do
- expect(element(skip_js: true)).not_to include 'script'
- end
-
it 'converts to Time' do
expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
end
- it 'add class for the short format and includes inline script' do
+ it 'add class for the short format' do
timeago_element = element(short_format: 'short')
- expect(timeago_element.attr('class')).to eq 'js-short-timeago js-timeago-pending'
- script_element = timeago_element.next_element
- expect(script_element.name).to eq 'script'
- end
-
- it 'add class for the short format and does not include inline script' do
- timeago_element = element(short_format: 'short', skip_js: true)
expect(timeago_element.attr('class')).to eq 'js-short-timeago'
expect(timeago_element.next_element).to eq nil
end
diff --git a/spec/helpers/components_helper_spec.rb b/spec/helpers/components_helper_spec.rb
new file mode 100644
index 00000000000..94a59193be8
--- /dev/null
+++ b/spec/helpers/components_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe ComponentsHelper do
+ describe '#gitlab_workhorse_version' do
+ context 'without a Gitlab-Workhorse header' do
+ it 'shows the version from Gitlab::Workhorse.version' do
+ expect(helper.gitlab_workhorse_version).to eq Gitlab::Workhorse.version
+ end
+ end
+
+ context 'with a Gitlab-Workhorse header' do
+ before do
+ helper.request.headers['Gitlab-Workhorse'] = '42.42.0-rc3'
+ end
+
+ it 'shows the actual GitLab Workhorse version currently in use' do
+ expect(helper.gitlab_workhorse_version).to eq '42.42.0'
+ end
+ end
+ end
+end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 9c7c79f57c6..837e7afa7e8 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -61,7 +61,7 @@ describe DiffHelper do
describe '#diff_line_content' do
it 'returns non breaking space when line is empty' do
- expect(diff_line_content(nil)).to eq(' &nbsp;')
+ expect(diff_line_content(nil)).to eq('&nbsp;')
end
it 'returns the line itself' do
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 5368e5fab06..1d494edcd3b 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
- to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 6703d88e357..ffca1c94da1 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -11,11 +11,11 @@ describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
@@ -32,10 +32,10 @@ describe MembersHelper do
describe '#remove_member_title' do
let(:requester) { build(:user) }
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:group_member) { build(:group_member, group: group) }
let(:group_member_request) { group.request_access(requester) }
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 2f9291afc3f..77841e85223 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -85,4 +85,45 @@ describe PreferencesHelper do
and_return(double('user', messages))
end
end
+
+ describe '#default_project_view' do
+ context 'user not signed in' do
+ before do
+ helper.instance_variable_set(:@project, project)
+ stub_user
+ end
+
+ context 'when repository is empty' do
+ let(:project) { create(:project_empty_repo, :public) }
+
+ it 'returns activity if user has repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+
+ it 'returns activity if user does not have repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+ end
+
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns readme if user has repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('readme')
+ end
+
+ it 'returns activity if user does not have repository access' do
+ allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('activity')
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 64aa41020c9..4b2ca3514f8 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -6,38 +6,6 @@ describe SearchHelper do
str
end
- describe 'parsing result' do
- let(:project) { create(:project) }
- let(:repository) { project.repository }
- let(:results) { repository.search_files('feature', 'master') }
- let(:search_result) { results.first }
-
- subject { helper.parse_search_result(search_result) }
-
- it "returns a valid OpenStruct object" do
- is_expected.to be_an OpenStruct
- expect(subject.filename).to eq('CHANGELOG')
- expect(subject.basename).to eq('CHANGELOG')
- expect(subject.ref).to eq('master')
- expect(subject.startline).to eq(188)
- expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
- end
-
- context "when filename has extension" do
- let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
-
- it { expect(subject.filename).to eq('CONTRIBUTE.md') }
- it { expect(subject.basename).to eq('CONTRIBUTE') }
- end
-
- context "when file under directory" do
- let(:search_result) { "master:a/b/c.md:5:a b c\n" }
-
- it { expect(subject.filename).to eq('a/b/c.md') }
- it { expect(subject.basename).to eq('a/b/c') }
- end
- end
-
describe 'search_autocomplete_source' do
context "with no current user" do
before do
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
new file mode 100644
index 00000000000..90388929612
--- /dev/null
+++ b/spec/javascripts/.eslintrc
@@ -0,0 +1,11 @@
+{
+ "plugins": ["jasmine"],
+ "env": {
+ "jasmine": true
+ },
+ "extends": "plugin:jasmine/recommended",
+ "rules": {
+ "prefer-arrow-callback": 0,
+ "func-names": 0
+ }
+}
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
index 16e908f3a81..7e38abc608e 100644
--- a/spec/javascripts/application_spec.js
+++ b/spec/javascripts/application_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, padded-blocks, max-len */
/*= require lib/utils/common_utils */
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 3d705e1cb2e..ac1404f6e1c 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
/*= require awards_handler */
/*= require jquery */
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 36254a7370e..b4573e53a4e 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */
/*= require behaviors/autosize */
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 7370ccb4a08..efb1203eb2f 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
/*= require behaviors/quick_submit */
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 32469a4fd1f..c3f4c867d6a 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
/*= require behaviors/requires_input */
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 6208c2386b0..b84dfc8197b 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -13,8 +13,9 @@
//= require boards/stores/boards_store
//= require ./mock_data
-(() => {
+describe('Store', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
@@ -24,145 +25,147 @@
});
});
- describe('Store', () => {
- it('starts with a blank state', () => {
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
- describe('lists', () => {
- it('creates new list without persisting to DB', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- });
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
- it('finds list by ID', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
- expect(list.id).toBe(1);
- });
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
- it('finds list by type', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+ expect(list.id).toBe(1);
+ });
- expect(list).toBeDefined();
- });
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
- it('finds list limited by type', () => {
- gl.issueBoards.BoardsStore.addList({
- id: 1,
- position: 0,
- title: 'Test',
- list_type: 'backlog'
- });
- const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+ expect(list).toBeDefined();
+ });
- expect(list).toBeDefined();
+ it('finds list limited by type', () => {
+ gl.issueBoards.BoardsStore.addList({
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'backlog'
});
+ const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
- it('gets issue when new list added', (done) => {
- gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
- setTimeout(() => {
- expect(list.issues.length).toBe(1);
- expect(list.issues[0].id).toBe(1);
- done();
- }, 0);
- });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- it('persists new list', (done) => {
- gl.issueBoards.BoardsStore.new({
- title: 'Test',
- type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;'
- }
- });
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
-
- setTimeout(() => {
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
- expect(list).toBeDefined();
- expect(list.id).toBe(1);
- expect(list.position).toBe(0);
- done();
- }, 0);
- });
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
- it('check for blank state adding', () => {
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ it('persists new list', (done) => {
+ gl.issueBoards.BoardsStore.new({
+ title: 'Test',
+ type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
});
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- it('check for blank state not adding', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
- });
+ setTimeout(() => {
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(list).toBeDefined();
+ expect(list.id).toBe(1);
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
- it('check for blank state adding when backlog & done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'backlog'
- });
- gl.issueBoards.BoardsStore.addList({
- list_type: 'done'
- });
+ it('check for blank state adding', () => {
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('check for blank state not adding', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
+ });
- expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ it('check for blank state adding when backlog & done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'backlog'
});
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
+ });
+
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
- it('adds the blank state', () => {
- gl.issueBoards.BoardsStore.addBlankState();
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
- const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
- expect(list).toBeDefined();
- });
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
- it('removes list from state', () => {
- gl.issueBoards.BoardsStore.addList(listObj);
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- gl.issueBoards.BoardsStore.removeList(1, 'label');
+ gl.issueBoards.BoardsStore.removeList(1, 'label');
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
- });
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
- it('moves the position of lists', () => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ it('moves the position of lists', () => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
- expect(listOne.position).toBe(1);
- });
+ expect(listOne.position).toBe(1);
+ });
- it('moves an issue from one list to another', (done) => {
- const listOne = gl.issueBoards.BoardsStore.addList(listObj),
- listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+ it('moves an issue from one list to another', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj),
+ listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
- expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- setTimeout(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
- gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
- done();
- }, 0);
- });
+ done();
+ }, 0);
});
});
-})();
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 1a0427fdd90..dfbcbe3a7c1 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -17,12 +17,17 @@ describe('List model', () => {
let list;
beforeEach(() => {
+ Vue.http.interceptors.push(boardsMockInterceptor);
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
});
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
it('gets issues when created', (done) => {
setTimeout(() => {
expect(list.issues.length).toBe(1);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 80d05e8a1a3..fcb3d8f17d8 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -48,10 +48,10 @@ const BoardsMockData = {
}
};
-Vue.http.interceptors.push((request, next) => {
+const boardsMockInterceptor = (request, next) => {
const body = BoardsMockData[request.method][request.url];
next(request.respondWith(JSON.stringify(body), {
status: 200
}));
-});
+};
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
new file mode 100644
index 00000000000..e21e5844a26
--- /dev/null
+++ b/spec/javascripts/build_spec.js.es6
@@ -0,0 +1,190 @@
+/* global Build */
+/* eslint-disable no-new */
+//= require lib/utils/timeago
+//= require lib/utils/datetime_utility
+//= require build
+//= require breakpoints
+//= require jquery.nicescroll
+//= require turbolinks
+
+(() => {
+ describe('Build', () => {
+ fixture.preload('build.html');
+
+ beforeEach(function () {
+ fixture.load('build.html');
+ spyOn($, 'ajax');
+ });
+
+ describe('constructor', () => {
+ beforeEach(function () {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ describe('setup', function () {
+ const removeDate = new Date();
+ removeDate.setUTCFullYear(removeDate.getUTCFullYear() + 1);
+ // give the test three days to run
+ removeDate.setTime(removeDate.getTime() + (3 * 24 * 60 * 60 * 1000));
+
+ beforeEach(function () {
+ const removeDateElement = document.querySelector('.js-artifacts-remove');
+ removeDateElement.innerText = removeDate.toString();
+
+ this.build = new Build();
+ });
+
+ it('copies build options', function () {
+ expect(this.build.pageUrl).toBe('http://example.com/root/test-build/builds/2');
+ expect(this.build.buildUrl).toBe('http://example.com/root/test-build/builds/2.json');
+ expect(this.build.buildStatus).toBe('passed');
+ expect(this.build.buildStage).toBe('test');
+ expect(this.build.state).toBe('buildstate');
+ });
+
+ it('only shows the jobs matching the current stage', function () {
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('selects the current stage in the build dropdown menu', function () {
+ expect($('.stage-selection').text()).toBe('test');
+ });
+
+ it('updates the jobs when the build dropdown changes', function () {
+ $('.stage-item:contains("build")').click();
+
+ expect($('.stage-selection').text()).toBe('build');
+ expect($('.build-job[data-stage="build"]').is(':visible')).toBe(true);
+ expect($('.build-job[data-stage="test"]').is(':visible')).toBe(false);
+ expect($('.build-job[data-stage="deploy"]').is(':visible')).toBe(false);
+ });
+
+ it('displays the remove date correctly', function () {
+ const removeDateElement = document.querySelector('.js-artifacts-remove');
+ expect(removeDateElement.innerText.trim()).toBe('1 year');
+ });
+ });
+
+ describe('initial build trace', function () {
+ beforeEach(function () {
+ new Build();
+ });
+
+ it('displays the initial build trace', function () {
+ expect($.ajax.calls.count()).toBe(1);
+ const [{ url, dataType, success, context }] = $.ajax.calls.argsFor(0);
+ expect(url).toBe('http://example.com/root/test-build/builds/2.json');
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Example/);
+ });
+
+ it('removes the spinner', function () {
+ const [{ success, context }] = $.ajax.calls.argsFor(0);
+ success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
+
+ expect($('.js-build-refresh').length).toBe(0);
+ });
+ });
+
+ describe('running build', function () {
+ beforeEach(function () {
+ $('.js-build-options').data('buildStatus', 'running');
+ this.build = new Build();
+ spyOn(this.build, 'location')
+ .and.returnValue('http://example.com/root/test-build/builds/2');
+ });
+
+ it('updates the build trace on an interval', function () {
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(2);
+ let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
+ expect(url).toBe(
+ 'http://example.com/root/test-build/builds/2/trace.json?state=buildstate'
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>Update<span>',
+ status: 'running',
+ state: 'newstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.build.state).toBe('newstate');
+
+ jasmine.clock().tick(4001);
+
+ expect($.ajax.calls.count()).toBe(3);
+ [{ url, dataType, success, context }] = $.ajax.calls.argsFor(2);
+ expect(url).toBe(
+ 'http://example.com/root/test-build/builds/2/trace.json?state=newstate'
+ );
+ expect(dataType).toBe('json');
+ expect(success).toEqual(jasmine.any(Function));
+
+ success.call(context, {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.build.state).toBe('finalstate');
+ });
+
+ it('replaces the entire build trace', function () {
+ jasmine.clock().tick(4001);
+ let [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Update</span>',
+ status: 'running',
+ append: true,
+ });
+
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ jasmine.clock().tick(4001);
+ [{ success, context }] = $.ajax.calls.argsFor(2);
+ success.call(context, {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ });
+
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ });
+
+ it('reloads the page when the build is done', function () {
+ spyOn(Turbolinks, 'visit');
+
+ jasmine.clock().tick(4001);
+ const [{ success, context }] = $.ajax.calls.argsFor(1);
+ success.call(context, {
+ html: '<span>Final</span>',
+ status: 'passed',
+ append: true,
+ });
+
+ expect(Turbolinks.visit).toHaveBeenCalledWith(
+ 'http://example.com/root/test-build/builds/2'
+ );
+ });
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
new file mode 100644
index 00000000000..c9ac7a73fd0
--- /dev/null
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -0,0 +1,37 @@
+//= require vue
+//= require environments/components/environment_actions
+
+describe('Actions Component', () => {
+ fixture.preload('environments/element.html');
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('Should render a dropdown with the provided actions', () => {
+ const actionsMock = [
+ {
+ name: 'bar',
+ play_path: 'https://gitlab.com/play',
+ },
+ {
+ name: 'foo',
+ play_path: '#',
+ },
+ ];
+
+ const component = new window.gl.environmentsList.ActionsComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ actions: actionsMock,
+ },
+ });
+
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length
+ ).toEqual(actionsMock.length);
+ expect(
+ component.$el.querySelector('.dropdown-menu li a').getAttribute('href')
+ ).toEqual(actionsMock[0].play_path);
+ });
+});
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
new file mode 100644
index 00000000000..156506ef28f
--- /dev/null
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -0,0 +1,22 @@
+//= require vue
+//= require environments/components/environment_external_url
+
+describe('External URL Component', () => {
+ fixture.preload('environments/element.html');
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('should link to the provided external_url', () => {
+ const externalURL = 'https://gitlab.com';
+ const component = new window.gl.environmentsList.ExternalUrlComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ external_url: externalURL,
+ },
+ });
+
+ expect(component.$el.getAttribute('href')).toEqual(externalURL);
+ expect(component.$el.querySelector('fa-external-link')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
new file mode 100644
index 00000000000..54c93367b17
--- /dev/null
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -0,0 +1,215 @@
+//= require vue
+//= require environments/components/environment_item
+
+describe('Environment item', () => {
+ fixture.preload('environments/table.html');
+ beforeEach(() => {
+ fixture.load('environments/table.html');
+ });
+
+ describe('When item is folder', () => {
+ let mockItem;
+ let component;
+
+ beforeEach(() => {
+ mockItem = {
+ name: 'review',
+ children: [
+ {
+ name: 'review-app',
+ id: 1,
+ state: 'available',
+ external_url: '',
+ last_deployment: {},
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ },
+ {
+ name: 'production',
+ id: 2,
+ state: 'available',
+ external_url: '',
+ last_deployment: {},
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ },
+ ],
+ };
+
+ component = new window.gl.environmentsList.EnvironmentItem({
+ el: document.querySelector('tr#environment-row'),
+ propsData: {
+ model: mockItem,
+ toggleRow: () => {},
+ canCreateDeployment: false,
+ canReadEnvironment: true,
+ },
+ });
+ });
+
+ it('Should render folder icon and name', () => {
+ expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
+ expect(component.$el.querySelector('.folder-icon')).toBeDefined();
+ });
+
+ it('Should render the number of children in a badge', () => {
+ expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length);
+ });
+ });
+
+ describe('when item is not folder', () => {
+ let environment;
+ let component;
+
+ beforeEach(() => {
+ environment = {
+ id: 31,
+ name: 'production',
+ state: 'stopped',
+ external_url: 'http://external.com',
+ environment_type: null,
+ last_deployment: {
+ id: 66,
+ iid: 6,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_path: 'root/ci-folders/tree/master',
+ },
+ tag: true,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1279,
+ name: 'deploy',
+ build_path: '/root/ci-folders/builds/1279',
+ retry_path: '/root/ci-folders/builds/1279/retry',
+ },
+ manual_actions: [
+ {
+ name: 'action',
+ play_path: '/play',
+ },
+ ],
+ },
+ 'stoppable?': true,
+ environment_path: 'root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-10T15:55:58.778Z',
+ };
+
+ component = new window.gl.environmentsList.EnvironmentItem({
+ el: document.querySelector('tr#environment-row'),
+ propsData: {
+ model: environment,
+ toggleRow: () => {},
+ canCreateDeployment: true,
+ canReadEnvironment: true,
+ },
+ });
+ });
+
+ it('should render environment name', () => {
+ expect(component.$el.querySelector('.environment-name').textContent).toEqual(environment.name);
+ });
+
+ describe('With deployment', () => {
+ it('should render deployment internal id', () => {
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent
+ ).toContain(environment.last_deployment.iid);
+
+ expect(
+ component.$el.querySelector('.deployment-column span').textContent
+ ).toContain('#');
+ });
+
+ describe('With user information', () => {
+ it('should render user avatar with link to profile', () => {
+ expect(
+ component.$el.querySelector('.js-deploy-user-container').getAttribute('href')
+ ).toEqual(environment.last_deployment.user.web_url);
+ });
+ });
+
+ describe('With build url', () => {
+ it('Should link to build url provided', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href')
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+
+ it('Should render deployable name and id', () => {
+ expect(
+ component.$el.querySelector('.build-link').getAttribute('href')
+ ).toEqual(environment.last_deployment.deployable.build_path);
+ });
+ });
+
+ describe('With commit information', () => {
+ it('should render commit component', () => {
+ expect(
+ component.$el.querySelector('.js-commit-component')
+ ).toBeDefined();
+ });
+ });
+ });
+
+ describe('With manual actions', () => {
+ it('Should render actions component', () => {
+ expect(
+ component.$el.querySelector('.js-manual-actions-container')
+ ).toBeDefined();
+ });
+ });
+
+ describe('With external URL', () => {
+ it('should render external url component', () => {
+ expect(
+ component.$el.querySelector('.js-external-url-container')
+ ).toBeDefined();
+ });
+ });
+
+ describe('With stop action', () => {
+ it('Should render stop action component', () => {
+ expect(
+ component.$el.querySelector('.js-stop-component-container')
+ ).toBeDefined();
+ });
+ });
+
+ describe('With retry action', () => {
+ it('Should render rollback component', () => {
+ expect(
+ component.$el.querySelector('.js-rollback-component-container')
+ ).toBeDefined();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
new file mode 100644
index 00000000000..29449bbbd9e
--- /dev/null
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -0,0 +1,48 @@
+//= require vue
+//= require environments/components/environment_rollback
+describe('Rollback Component', () => {
+ fixture.preload('environments/element.html');
+
+ const retryURL = 'https://gitlab.com/retry';
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+ });
+
+ it('Should link to the provided retry_url', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retry_url: retryURL,
+ is_last_deployment: true,
+ },
+ });
+
+ expect(component.$el.getAttribute('href')).toEqual(retryURL);
+ });
+
+ it('Should render Re-deploy label when is_last_deployment is true', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retry_url: retryURL,
+ is_last_deployment: true,
+ },
+ });
+
+ expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
+ });
+
+
+ it('Should render Rollback label when is_last_deployment is false', () => {
+ const component = new window.gl.environmentsList.RollbackComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ retry_url: retryURL,
+ is_last_deployment: false,
+ },
+ });
+
+ expect(component.$el.querySelector('span').textContent).toContain('Rollback');
+ });
+});
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
new file mode 100644
index 00000000000..b842be4da61
--- /dev/null
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -0,0 +1,28 @@
+//= require vue
+//= require environments/components/environment_stop
+describe('Stop Component', () => {
+ fixture.preload('environments/element.html');
+
+ let stopURL;
+ let component;
+
+ beforeEach(() => {
+ fixture.load('environments/element.html');
+
+ stopURL = '/stop';
+ component = new window.gl.environmentsList.StopComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ stop_url: stopURL,
+ },
+ });
+ });
+
+ it('should link to the provided URL', () => {
+ expect(component.$el.getAttribute('href')).toEqual(stopURL);
+ });
+
+ it('should have a data-confirm attribute', () => {
+ expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
new file mode 100644
index 00000000000..82d9599f372
--- /dev/null
+++ b/spec/javascripts/environments/environments_store_spec.js.es6
@@ -0,0 +1,69 @@
+//= require vue
+//= require environments/stores/environments_store
+//= require ./mock_data
+/* globals environmentsList */
+(() => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.create();
+ });
+
+ describe('Store', () => {
+ it('should start with a blank state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0);
+ expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0);
+ expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0);
+ });
+
+ describe('store environments', () => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
+ });
+
+ it('should count stopped environments and save the count in the state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
+ });
+
+ it('should count available environments and save the count in the state', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
+ });
+
+ it('should store environments with same environment_type as sibilings', () => {
+ expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
+
+ const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
+ .filter(env => env.children && env.children.length > 0);
+
+ expect(parentFolder[0].children.length).toBe(2);
+ expect(parentFolder[0].children[0].environment_type).toBe('review');
+ expect(parentFolder[0].children[1].environment_type).toBe('review');
+ expect(parentFolder[0].children[0].name).toBe('test-environment');
+ expect(parentFolder[0].children[1].name).toBe('test-environment-1');
+ });
+
+ it('should sort the environments alphabetically', () => {
+ const { environments } = gl.environmentsList.EnvironmentsStore.state;
+
+ expect(environments[0].name).toBe('production');
+ expect(environments[1].name).toBe('review');
+ expect(environments[1].children[0].name).toBe('test-environment');
+ expect(environments[1].children[1].name).toBe('test-environment-1');
+ expect(environments[2].name).toBe('review_app');
+ });
+ });
+
+ describe('toggleFolder', () => {
+ beforeEach(() => {
+ gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
+ });
+
+ it('should toggle the open property for the given environment', () => {
+ gl.environmentsList.EnvironmentsStore.toggleFolder('review');
+
+ const { environments } = gl.environmentsList.EnvironmentsStore.state;
+ const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
+
+ expect(environment[0].isOpen).toBe(true);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
new file mode 100644
index 00000000000..9e16bc3e6a5
--- /dev/null
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -0,0 +1,135 @@
+/* eslint-disable no-unused-vars */
+const environmentsList = [
+ {
+ id: 31,
+ name: 'production',
+ state: 'available',
+ external_url: 'https://www.gitlab.com',
+ environment_type: null,
+ last_deployment: {
+ id: 64,
+ iid: 5,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
+ },
+ tag: false,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1278,
+ name: 'build',
+ build_path: '/root/ci-folders/builds/1278',
+ retry_path: '/root/ci-folders/builds/1278/retry',
+ },
+ manual_actions: [],
+ },
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 32,
+ name: 'review_app',
+ state: 'stopped',
+ external_url: 'https://www.gitlab.com',
+ environment_type: null,
+ last_deployment: {
+ id: 64,
+ iid: 5,
+ sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
+ },
+ tag: false,
+ 'last?': true,
+ user: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit: {
+ id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ short_id: '500aabcb',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2016-11-07T18:28:13.000+00:00',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
+ },
+ deployable: {
+ id: 1278,
+ name: 'build',
+ build_path: '/root/ci-folders/builds/1278',
+ retry_path: '/root/ci-folders/builds/1278/retry',
+ },
+ manual_actions: [],
+ },
+ 'stoppable?': false,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 33,
+ name: 'test-environment',
+ state: 'available',
+ environment_type: 'review',
+ last_deployment: null,
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+ {
+ id: 34,
+ name: 'test-environment-1',
+ state: 'available',
+ environment_type: 'review',
+ last_deployment: null,
+ 'stoppable?': true,
+ environment_path: '/root/ci-folders/environments/31',
+ created_at: '2016-11-07T11:11:16.525Z',
+ updated_at: '2016-11-07T11:11:16.525Z',
+ },
+];
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
index f28983d7764..c56e6c7789b 100644
--- a/spec/javascripts/extensions/array_spec.js
+++ b/spec/javascripts/extensions/array_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
/*= require extensions/array */
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index 9c361bb0867..76309930f27 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, padded-blocks */
/*= require extensions/jquery */
diff --git a/spec/javascripts/fixtures/build.html.haml b/spec/javascripts/fixtures/build.html.haml
new file mode 100644
index 00000000000..27136beb14c
--- /dev/null
+++ b/spec/javascripts/fixtures/build.html.haml
@@ -0,0 +1,62 @@
+.build-page
+ .prepend-top-default
+ .autoscroll-container
+ %button.btn.btn-success.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} enable autoscroll
+ #js-build-scroll.scroll-controls
+ %a.btn{href: '#build-trace'}
+ %i.fa.fa-angle-up
+ %a.btn{href: '#down-build-trace'}
+ %i.fa.fa-angle-down
+ %pre.build-trace#build-trace
+ %code.bash.js-build-output
+ %i.fa.fa-refresh.fa-spin.js-build-refresh
+
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+ .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
+ Build
+ %strong #1
+ %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
+ %i.fa.fa-angle-double-right
+ .blocks-container
+ .dropdown.build-dropdown
+ .title Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ %i.fa.fa-caret-down
+ %ul.dropdown-menu
+ %li
+ %a.stage-item build
+ %li
+ %a.stage-item test
+ %li
+ %a.stage-item deploy
+ .builds-container
+ .build-job{data: {stage: 'build'}}
+ %a{href: 'http://example.com/root/test-build/builds/1'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Setup
+ .build-job{data: {stage: 'test'}}
+ %a{href: 'http://example.com/root/test-build/builds/2'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Tests
+ .build-job{data: {stage: 'deploy'}}
+ %a{href: 'http://example.com/root/test-build/builds/3'}
+ %i.fa.fa-check
+ %i.fa.fa-check-circle-o
+ %span
+ Deploy
+
+.js-build-options{ data: { page_url: 'http://example.com/root/test-build/builds/2',
+ build_url: 'http://example.com/root/test-build/builds/2.json',
+ build_status: 'passed',
+ build_stage: 'test',
+ state1: 'buildstate' }}
+
+%p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove
+ 2016-12-19 09:02:12 UTC
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 41cf40c29cf..3d776bb9277 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, padded-blocks */
(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>";
diff --git a/spec/javascripts/fixtures/environments/element.html.haml b/spec/javascripts/fixtures/environments/element.html.haml
new file mode 100644
index 00000000000..8d7aeb23356
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/element.html.haml
@@ -0,0 +1 @@
+.test-dom-element
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
new file mode 100644
index 00000000000..d89bc50c1f0
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/environments.html.haml
@@ -0,0 +1,9 @@
+%div
+ #environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
+ "can-create-deployment" => "true",
+ "can-read-environment" => "true",
+ "can-create-environment" => "true",
+ "project-environments-path" => "https://gitlab.com/foo/environments",
+ "project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped",
+ "new-environment-path" => "https://gitlab.com/foo/environments/new",
+ "help-page-path" => "https://gitlab.com/help_page"}}
diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
new file mode 100644
index 00000000000..1ea1725c561
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/table.html.haml
@@ -0,0 +1,11 @@
+%table
+ %thead
+ %tr
+ %th Environment
+ %th Last deployment
+ %th Build
+ %th Commit
+ %th
+ %th
+ %tbody
+ %tr#environment-row
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 8c66c45ba79..a406e6cc36a 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 */
+/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 920e4ee0892..96f39abe13e 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */
//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index ae2821ecad9..f78573b992b 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable quotes, padded-blocks, no-undef, semi */
//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 9a859655d8b..d2bcbc37b64 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, padded-blocks, no-var */
/*= require header */
/*= require lib/utils/text_utility */
/*= require jquery */
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 949114185cf..beef46122ab 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
/*= require lib/utils/text_utility */
/*= require issue */
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index e0192a2d624..b8b174a2e53 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, 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 */
/*= require line_highlighter */
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 83d279ab414..cbe2634d3a4 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */
/*= require merge_request */
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 6a53c6aa6ac..971222c44e1 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, comma-dangle, dot-notation, quotes, no-undef, no-return-assign, no-underscore-dangle, camelcase, padded-blocks, max-len */
/*= require merge_request_tabs */
//= require breakpoints
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index 49dfeab61d8..575b87e6f17 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,6 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
/*= require merge_request_widget */
-/*= require jquery.timeago.js */
+/*= require lib/utils/timeago.js */
(function() {
describe('MergeRequestWidget', function() {
@@ -35,9 +35,9 @@
external_url_formatted: 'test-url.com'
}];
- spyOn(jQuery, 'getJSON').and.callFake((req, cb) => {
+ spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) {
cb(this.ciEnvironmentsStatusData);
- });
+ }.bind(this));
});
it('should call renderEnvironments when the environments property is set', function() {
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index c092424ec32..8828970d984 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */
/*= require jquery-ui/autocomplete */
/*= require new_branch_form */
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2e3a4b66e2d..51f2ae8bcbd 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
/*= require notes */
/*= require autosize */
/*= require gl_form */
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 1963857bba3..49211a6b852 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */
/*= require bootstrap */
/*= require select2 */
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index ef03d1147de..83ebbd63f3a 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
/*= require right_sidebar */
/*= require jquery */
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 29080804960..1b7f642d59e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
/*= require gl_dropdown */
/*= require search_autocomplete */
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 1f36a048153..7d36d79b687 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */
/*= require shortcuts_issuable */
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 9cb8243ee2c..8a64de4dd43 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren */
// PhantomJS (Teaspoons default driver) doesn't have support for
// Function.prototype.bind, which has caused confusion. Use this polyfill to
// avoid the confusion.
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 498f0f06797..ac411f6c306 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */
/*= require syntax_highlight */
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 024a91f0a80..944df6d23f7 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
/*= require u2f/authenticate */
/*= require u2f/util */
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index ad133682fb1..1459f968c3d 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* 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 */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index abea76f622f..0c73c5772bd 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
/*= require u2f/register */
/*= require u2f/util */
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6
new file mode 100644
index 00000000000..0e3b82967c1
--- /dev/null
+++ b/spec/javascripts/vue_common_components/commit_spec.js.es6
@@ -0,0 +1,126 @@
+//= require vue_common_component/commit
+
+describe('Commit component', () => {
+ let props;
+ let component;
+
+ it('should render a code-fork icon if it does not represent a tag', () => {
+ fixture.set('<div class="test-commit-container"></div>');
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: {
+ tag: false,
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ short_sha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
+ });
+
+ describe('Given all the props', () => {
+ beforeEach(() => {
+ fixture.set('<div class="test-commit-container"></div>');
+
+ props = {
+ tag: true,
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ short_sha: 'b7836edd',
+ title: 'Commit message',
+ author: {
+ avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
+ web_url: 'https://gitlab.com/jschatz1',
+ username: 'jschatz1',
+ },
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+ });
+
+ it('should render a tag icon if it represents a tag', () => {
+ expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
+ });
+
+ it('should render a link to the ref url', () => {
+ expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.ref.ref_url);
+ });
+
+ it('should render the ref name', () => {
+ expect(component.$el.querySelector('.branch-name').textContent).toContain(props.ref.name);
+ });
+
+ it('should render the commit short sha with a link to the commit url', () => {
+ expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commit_url);
+ expect(component.$el.querySelector('.commit-id').textContent).toContain(props.short_sha);
+ });
+
+ describe('Given commit title and author props', () => {
+ it('Should render a link to the author profile', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href')
+ ).toEqual(props.author.web_url);
+ });
+
+ it('Should render the author avatar with title and alt attributes', () => {
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title')
+ ).toContain(props.author.username);
+ expect(
+ component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt')
+ ).toContain(`${props.author.username}'s avatar`);
+ });
+ });
+
+ it('should render the commit title', () => {
+ expect(
+ component.$el.querySelector('a.commit-row-message').getAttribute('href')
+ ).toEqual(props.commit_url);
+ expect(
+ component.$el.querySelector('a.commit-row-message').textContent
+ ).toContain(props.title);
+ });
+ });
+
+ describe('When commit title is not provided', () => {
+ it('Should render default message', () => {
+ fixture.set('<div class="test-commit-container"></div>');
+ props = {
+ tag: false,
+ ref: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ short_sha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = new window.gl.CommitComponent({
+ el: document.querySelector('.test-commit-container'),
+ propsData: props,
+ });
+
+ expect(
+ component.$el.querySelector('.commit-title span').textContent
+ ).toContain('Cant find HEAD commit for this branch');
+ });
+ });
+});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 65b6e3dce33..a18e8aee9b1 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
/*= require zen_mode */
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index dca7f997570..a6d2ea11fcc 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -99,6 +99,28 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(doc.at_css('a')['href']).to eq link
end
+ it 'autolinks rdar' do
+ link = 'rdar://localhost.com/blah'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'does not autolink javascript' do
+ link = 'javascript://alert(document.cookie);'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a')).to be_nil
+ end
+
+ it 'does not autolink bad URLs' do
+ link = 'foo://23423:::asdf'
+ doc = filter("See #{link}")
+
+ expect(doc.to_s).to eq("See #{link}")
+ end
+
it 'does not include trailing punctuation' do
doc = filter("See #{link}.")
expect(doc.at_css('a').text).to eq link
diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index f181125156b..0140a91c7ba 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -28,31 +28,39 @@ describe Banzai::Filter::RedactorFilter, lib: true do
and_return(parser_class)
end
- it 'removes unpermitted Project references' do
- user = create(:user)
- project = create(:empty_project)
+ context 'valid projects' do
+ before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(true) }
- link = reference_link(project: project.id, reference_type: 'test')
- doc = filter(link, current_user: user)
+ it 'allows permitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ project.team << [user, :master]
+
+ link = reference_link(project: project.id, reference_type: 'test')
+ doc = filter(link, current_user: user)
- expect(doc.css('a').length).to eq 0
+ expect(doc.css('a').length).to eq 1
+ end
end
- it 'allows permitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- project.team << [user, :master]
+ context 'invalid projects' do
+ before { allow_any_instance_of(Banzai::ReferenceParser::BaseParser).to receive(:can_read_reference?).and_return(false) }
- link = reference_link(project: project.id, reference_type: 'test')
- doc = filter(link, current_user: user)
+ it 'removes unpermitted references' do
+ user = create(:user)
+ project = create(:empty_project)
- expect(doc.css('a').length).to eq 1
- end
+ link = reference_link(project: project.id, reference_type: 'test')
+ doc = filter(link, current_user: user)
- it 'handles invalid Project references' do
- link = reference_link(project: 12345, reference_type: 'test')
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'handles invalid references' do
+ link = reference_link(project: 12345, reference_type: 'test')
- expect { filter(link) }.not_to raise_error
+ expect { filter(link) }.not_to raise_error
+ end
end
end
diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb
index 9095d2b1345..aa127f0179d 100644
--- a/spec/lib/banzai/reference_parser/base_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb
@@ -27,41 +27,12 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do
let(:link) { empty_html_link }
context 'when the link has a data-project attribute' do
- it 'returns the nodes if the attribute value equals the current project ID' do
+ it 'checks if user can read the resource' do
link['data-project'] = project.id.to_s
- expect(Ability).not_to receive(:allowed?)
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
- end
-
- it 'returns the nodes if the user can read the project' do
- other_project = create(:empty_project, :public)
-
- link['data-project'] = other_project.id.to_s
-
- expect(Ability).to receive(:allowed?).
- with(user, :read_project, other_project).
- and_return(true)
-
- expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
- end
-
- it 'returns an empty Array when the attribute value is empty' do
- link['data-project'] = ''
-
- expect(subject.nodes_visible_to_user(user, [link])).to eq([])
- end
-
- it 'returns an empty Array when the user can not read the project' do
- other_project = create(:empty_project, :public)
-
- link['data-project'] = other_project.id.to_s
-
- expect(Ability).to receive(:allowed?).
- with(user, :read_project, other_project).
- and_return(false)
+ expect(subject).to receive(:can_read_reference?).with(user, project)
- expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ subject.nodes_visible_to_user(user, [link])
end
end
diff --git a/spec/lib/banzai/reference_parser/commit_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
index 0b76d29fce0..412ffa77c36 100644
--- a/spec/lib/banzai/reference_parser/commit_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-commit'] = 123 }
+
+ it_behaves_like "referenced feature visibility", "repository"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
index ba982f38542..96e55b0997a 100644
--- a/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/commit_range_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::CommitRangeParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-commit-range'] = '123..456' }
+
+ it_behaves_like "referenced feature visibility", "repository"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
index a6ef8394fe7..50a5d1a19ba 100644
--- a/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/external_issue_parser_spec.rb
@@ -8,6 +8,14 @@ describe Banzai::ReferenceParser::ExternalIssueParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-external-issue'] = 123 }
+
+ it_behaves_like "referenced feature visibility", "issues"
+ end
+ end
+
describe '#referenced_by' do
context 'when the link has a data-project attribute' do
before do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 85cfe728b6a..6873b7b85f9 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -4,10 +4,10 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
include ReferenceParserHelpers
let(:project) { create(:empty_project, :public) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
- subject { described_class.new(project, user) }
- let(:link) { empty_html_link }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:link) { empty_html_link }
+ subject { described_class.new(project, user) }
describe '#nodes_visible_to_user' do
context 'when the link has a data-issue attribute' do
@@ -15,6 +15,8 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
link['data-issue'] = issue.id.to_s
end
+ it_behaves_like "referenced feature visibility", "issues"
+
it 'returns the nodes when the user can read the issue' do
expect(Ability).to receive(:issues_readable_by_user).
with([issue], user).
diff --git a/spec/lib/banzai/reference_parser/label_parser_spec.rb b/spec/lib/banzai/reference_parser/label_parser_spec.rb
index 77fda47f0e7..8c540d35ddd 100644
--- a/spec/lib/banzai/reference_parser/label_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/label_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::LabelParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-label'] = label.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-label attribute' do
context 'using an existing label ID' do
diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
index cf89ad598ea..cb69ca16800 100644
--- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb
@@ -8,6 +8,19 @@ describe Banzai::ReferenceParser::MergeRequestParser, lib: true do
subject { described_class.new(merge_request.target_project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ let(:project) { merge_request.target_project }
+
+ before do
+ project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ link['data-merge-request'] = merge_request.id.to_s
+ end
+
+ it_behaves_like "referenced feature visibility", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-merge-request attribute' do
context 'using an existing merge request ID' do
diff --git a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
index 6aa45a22cc4..2d4d589ae34 100644
--- a/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/milestone_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::MilestoneParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-milestone'] = milestone.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "issues", "merge_requests"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-milestone attribute' do
context 'using an existing milestone ID' do
diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
index 59127b7c5d1..d217a775802 100644
--- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb
@@ -9,6 +9,14 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do
subject { described_class.new(project, user) }
let(:link) { empty_html_link }
+ describe '#nodes_visible_to_user' do
+ context 'when the link has a data-issue attribute' do
+ before { link['data-snippet'] = snippet.id.to_s }
+
+ it_behaves_like "referenced feature visibility", "snippets"
+ end
+ end
+
describe '#referenced_by' do
describe 'when the link has a data-snippet attribute' do
context 'using an existing snippet ID' do
diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb
index 4e7f82a6e09..fafc2cec546 100644
--- a/spec/lib/banzai/reference_parser/user_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb
@@ -103,6 +103,8 @@ describe Banzai::ReferenceParser::UserParser, lib: true do
it 'returns the nodes if the attribute value equals the current project ID' do
link['data-project'] = project.id.to_s
+ # Ensure that we dont call for Ability.allowed?
+ # When project_id in the node is equal to current project ID
expect(Ability).not_to receive(:allowed?)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 84f21631719..ff5dcc06ab3 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1124,8 +1124,8 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
- it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
- config = YAML.dump({ extra: { services: "test" } })
+ it "returns errors if services configuration is not correct" do
+ config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings")
diff --git a/spec/lib/constraints/constrainer_helper_spec.rb b/spec/lib/constraints/constrainer_helper_spec.rb
new file mode 100644
index 00000000000..27c8d72aefc
--- /dev/null
+++ b/spec/lib/constraints/constrainer_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe ConstrainerHelper, lib: true do
+ include ConstrainerHelper
+
+ describe '#extract_resource_path' do
+ it { expect(extract_resource_path('/gitlab/')).to eq('gitlab') }
+ it { expect(extract_resource_path('///gitlab//')).to eq('gitlab') }
+ it { expect(extract_resource_path('/gitlab.atom')).to eq('gitlab') }
+
+ context 'relative url' do
+ before do
+ allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
+ end
+
+ it { expect(extract_resource_path('/gitlab/foo')).to eq('foo') }
+ it { expect(extract_resource_path('/foo/bar')).to eq('foo/bar') }
+ end
+ end
+end
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
index f0b75a664f2..42299b17c2b 100644
--- a/spec/lib/constraints/group_url_constrainer_spec.rb
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -1,10 +1,19 @@
require 'spec_helper'
describe GroupUrlConstrainer, lib: true do
- let!(:username) { create(:group, path: 'gitlab-org') }
+ let!(:group) { create(:group, path: 'gitlab') }
- describe '#find_resource' do
- it { expect(!!subject.find_resource('gitlab-org')).to be_truthy }
- it { expect(!!subject.find_resource('gitlab-com')).to be_falsey }
+ describe '#matches?' do
+ context 'root group' do
+ it { expect(subject.matches?(request '/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/edit')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
+ end
+ end
+
+ def request(path)
+ double(:request, path: path)
end
end
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
deleted file mode 100644
index 7814711fe27..00000000000
--- a/spec/lib/constraints/namespace_url_constrainer_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe NamespaceUrlConstrainer, lib: true do
- let!(:group) { create(:group, path: 'gitlab') }
-
- describe '#matches?' do
- context 'existing namespace' do
- it { expect(subject.matches?(request '/gitlab')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab/')).to be_truthy }
- it { expect(subject.matches?(request '//gitlab/')).to be_truthy }
- end
-
- context 'non-existing namespace' do
- it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
- it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey }
- it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
- it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
- end
-
- context 'relative url' do
- before do
- allow(Gitlab::Application.config).to receive(:relative_url_root) { '/gitlab' }
- end
-
- it { expect(subject.matches?(request '/gitlab/gitlab')).to be_truthy }
- it { expect(subject.matches?(request '/gitlab/gitlab-ce')).to be_falsey }
- it { expect(subject.matches?(request '/gitlab/')).to be_falsey }
- end
- end
-
- def request(path)
- OpenStruct.new(path: path)
- end
-end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
index 4b26692672f..b3f8530c609 100644
--- a/spec/lib/constraints/user_url_constrainer_spec.rb
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -3,8 +3,14 @@ require 'spec_helper'
describe UserUrlConstrainer, lib: true do
let!(:username) { create(:user, username: 'dz') }
- describe '#find_resource' do
- it { expect(!!subject.find_resource('dz')).to be_truthy }
- it { expect(!!subject.find_resource('john')).to be_falsey }
+ describe '#matches?' do
+ it { expect(subject.matches?(request '/dz')).to be_truthy }
+ it { expect(subject.matches?(request '/dz.atom')).to be_truthy }
+ it { expect(subject.matches?(request '/dz/projects')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab')).to be_falsey }
+ end
+
+ def request(path)
+ double(:request, path: path)
end
end
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
new file mode 100644
index 00000000000..8cedbb0240f
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Command, service: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(project, user, params).execute }
+
+ describe '#execute' do
+ context 'when no command is available' do
+ let(:params) { { text: 'issue show 1' } }
+ let(:project) { create(:project, has_external_issue_tracker: true) }
+
+ it 'displays 404 messages' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('404 not found')
+ end
+ end
+
+ context 'when an unknown command is triggered' do
+ let(:params) { { command: '/gitlab', text: "unknown command 123" } }
+
+ it 'displays the help message' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('Available commands')
+ expect(subject[:text]).to match('/gitlab issue show')
+ end
+ end
+
+ context 'the user can not create an issue' do
+ let(:params) { { text: "issue create my new issue" } }
+
+ it 'rejects the actions' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+ end
+ end
+
+ context 'issue is successfully created' do
+ let(:params) { { text: "issue create my new issue" } }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'presents the issue' do
+ expect(subject[:text]).to match("my new issue")
+ end
+
+ it 'shows a link to the new issue' do
+ expect(subject[:text]).to match(/\/issues\/\d+/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb
new file mode 100644
index 00000000000..df0c317ccea
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueCreate, service: true do
+ describe '#execute' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:regex_match) { described_class.match("issue create bird is the word") }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'without description' do
+ it 'creates the issue' do
+ expect { subject }.to change { project.issues.count }.by(1)
+
+ expect(subject.title).to eq('bird is the word')
+ end
+ end
+
+ context 'with description' do
+ let(:description) { "Surfin bird" }
+ let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") }
+
+ it 'creates the issue with description' do
+ subject
+
+ expect(Issue.last.description).to eq(description)
+ end
+ end
+ end
+
+ describe '.match' do
+ it 'matches the title without description' do
+ match = described_class.match("issue create my title")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq("")
+ end
+
+ it 'matches the title with description' do
+ match = described_class.match("issue create my title\n\ndescription")
+
+ expect(match[:title]).to eq('my title')
+ expect(match[:description]).to eq('description')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
new file mode 100644
index 00000000000..331a4604e9b
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::IssueShow, service: true do
+ describe '#execute' do
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:user) { issue.author }
+ let(:regex_match) { described_class.match("issue show #{issue.iid}") }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ subject do
+ described_class.new(project, user).execute(regex_match)
+ end
+
+ context 'the issue exists' do
+ it 'returns the issue' do
+ expect(subject.iid).to be issue.iid
+ end
+ end
+
+ context 'the issue does not exist' do
+ let(:regex_match) { described_class.match("issue show 2343242") }
+
+ it "returns nil" do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ describe 'self.match' do
+ it 'matches the iid' do
+ match = described_class.match("issue show 123")
+
+ expect(match[:iid]).to eq("123")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_name_token_spec.rb b/spec/lib/gitlab/chat_name_token_spec.rb
new file mode 100644
index 00000000000..8c1e6efa9db
--- /dev/null
+++ b/spec/lib/gitlab/chat_name_token_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatNameToken, lib: true do
+ context 'when using unknown token' do
+ let(:token) { }
+
+ subject { described_class.new(token).get }
+
+ it 'returns empty data' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when storing data' do
+ let(:data) { { key: 'value' } }
+
+ subject { described_class.new(@token) }
+
+ before do
+ @token = described_class.new.store!(data)
+ end
+
+ it 'returns stored data' do
+ expect(subject.get).to eq(data)
+ end
+
+ context 'and after deleting them' do
+ before do
+ subject.delete
+ end
+
+ it 'data are removed' do
+ expect(subject.get).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index c09a0a9c793..5c31423fdee 100644
--- a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Artifacts do
+describe Gitlab::Ci::Config::Entry::Artifacts do
let(:entry) { described_class.new(config) }
describe 'validation' do
diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
index 24d9daafd88..fde03c51e2c 100644
--- a/spec/lib/gitlab/ci/config/node/attributable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Attributable do
+describe Gitlab::Ci::Config::Entry::Attributable do
let(:node) { Class.new }
let(:instance) { node.new }
diff --git a/spec/lib/gitlab/ci/config/node/boolean_spec.rb b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
index deafa8bf8a7..5f067cad93c 100644
--- a/spec/lib/gitlab/ci/config/node/boolean_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Boolean do
+describe Gitlab::Ci::Config::Entry::Boolean do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index e251210949c..70a327c5183 100644
--- a/spec/lib/gitlab/ci/config/node/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Cache do
+describe Gitlab::Ci::Config::Entry::Cache do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
index e373c40706f..b8b0825a1c7 100644
--- a/spec/lib/gitlab/ci/config/node/commands_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/commands_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Commands do
+describe Gitlab::Ci::Config::Entry::Commands do
let(:entry) { described_class.new(config) }
context 'when entry config value is an array' do
diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
new file mode 100644
index 00000000000..ae7e628b5b5
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Configurable do
+ let(:entry) { Class.new }
+
+ before do
+ entry.include(described_class)
+ end
+
+ describe 'validations' do
+ let(:validator) { entry.validator.new(instance) }
+
+ before do
+ entry.class_eval do
+ attr_reader :config
+
+ def initialize(config)
+ @config = config
+ end
+ end
+
+ validator.validate
+ end
+
+ context 'when entry validator is invalid' do
+ let(:instance) { entry.new('ls') }
+
+ it 'returns invalid validator' do
+ expect(validator).to be_invalid
+ end
+ end
+
+ context 'when entry instance is valid' do
+ let(:instance) { entry.new(key: 'value') }
+
+ it 'returns valid validator' do
+ expect(validator).to be_valid
+ end
+ end
+ end
+
+ describe 'configured entries' do
+ before do
+ entry.class_eval do
+ entry :object, Object, description: 'test object'
+ end
+ end
+
+ describe '.nodes' do
+ it 'has valid nodes' do
+ expect(entry.nodes).to include :object
+ end
+
+ it 'creates a node factory' do
+ expect(entry.nodes[:object])
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory
+ end
+
+ it 'returns a duplicated factory object' do
+ first_factory = entry.nodes[:object]
+ second_factory = entry.nodes[:object]
+
+ expect(first_factory).not_to be_equal(second_factory)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/node/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index df925ff1afd..d97806295fb 100644
--- a/spec/lib/gitlab/ci/config/node/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Environment do
+describe Gitlab::Ci::Config::Entry::Environment do
let(:entry) { described_class.new(config) }
before { entry.compose! }
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
index a699089c563..00dad5d9591 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Factory do
+describe Gitlab::Ci::Config::Entry::Factory do
describe '#create!' do
- let(:factory) { described_class.new(node) }
- let(:node) { Gitlab::Ci::Config::Node::Script }
+ let(:factory) { described_class.new(entry) }
+ let(:entry) { Gitlab::Ci::Config::Entry::Script }
context 'when setting a concrete value' do
it 'creates entry with valid value' do
@@ -54,7 +54,7 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when not setting a value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
- Gitlab::Ci::Config::Node::Factory::InvalidFactory
+ Gitlab::Ci::Config::Entry::Factory::InvalidFactory
)
end
end
@@ -66,12 +66,12 @@ describe Gitlab::Ci::Config::Node::Factory do
.create!
expect(entry)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
end
end
context 'when passing metadata' do
- let(:node) { spy('node') }
+ let(:entry) { spy('entry') }
it 'passes metadata as a parameter' do
factory
@@ -79,7 +79,7 @@ describe Gitlab::Ci::Config::Node::Factory do
.metadata(some: 'hash')
.create!
- expect(node).to have_received(:new)
+ expect(entry).to have_received(:new)
.with('some value', { some: 'hash' })
end
end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 12232ff7e2f..5e5c5dcc385 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Global do
+describe Gitlab::Ci::Config::Entry::Global do
let(:global) { described_class.new(hash) }
describe '.nodes' do
@@ -40,9 +40,9 @@ describe Gitlab::Ci::Config::Node::Global do
it 'creates node object using valid class' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Script
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Script
expect(global.descendants.second)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Image
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Image
end
it 'sets correct description for nodes' do
@@ -60,9 +60,9 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when not composed' do
- describe '#before_script' do
+ describe '#before_script_value' do
it 'returns nil' do
- expect(global.before_script).to be nil
+ expect(global.before_script_value).to be nil
end
end
@@ -82,40 +82,40 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- describe '#before_script' do
+ describe '#before_script_value' do
it 'returns correct script' do
- expect(global.before_script).to eq ['ls', 'pwd']
+ expect(global.before_script_value).to eq ['ls', 'pwd']
end
end
- describe '#image' do
+ describe '#image_value' do
it 'returns valid image' do
- expect(global.image).to eq 'ruby:2.2'
+ expect(global.image_value).to eq 'ruby:2.2'
end
end
- describe '#services' do
+ describe '#services_value' do
it 'returns array of services' do
- expect(global.services).to eq ['postgres:9.1', 'mysql:5.5']
+ expect(global.services_value).to eq ['postgres:9.1', 'mysql:5.5']
end
end
- describe '#after_script' do
+ describe '#after_script_value' do
it 'returns after script' do
- expect(global.after_script).to eq ['make clean']
+ expect(global.after_script_value).to eq ['make clean']
end
end
- describe '#variables' do
+ describe '#variables_value' do
it 'returns variables' do
- expect(global.variables).to eq(VAR: 'value')
+ expect(global.variables_value).to eq(VAR: 'value')
end
end
- describe '#stages' do
+ describe '#stages_value' do
context 'when stages key defined' do
it 'returns array of stages' do
- expect(global.stages).to eq %w[build pages]
+ expect(global.stages_value).to eq %w[build pages]
end
end
@@ -126,21 +126,21 @@ describe Gitlab::Ci::Config::Node::Global do
end
it 'returns array of types as stages' do
- expect(global.stages).to eq %w[test deploy]
+ expect(global.stages_value).to eq %w[test deploy]
end
end
end
- describe '#cache' do
+ describe '#cache_value' do
it 'returns cache configuration' do
- expect(global.cache)
+ expect(global.cache_value)
.to eq(key: 'k', untracked: true, paths: ['public/'])
end
end
- describe '#jobs' do
+ describe '#jobs_value' do
it 'returns jobs configuration' do
- expect(global.jobs).to eq(
+ expect(global.jobs_value).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
before_script: ['ls', 'pwd'],
@@ -181,25 +181,25 @@ describe Gitlab::Ci::Config::Node::Global do
it 'contains unspecified nodes' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
end
end
- describe '#variables' do
+ describe '#variables_value' do
it 'returns default value for variables' do
- expect(global.variables).to eq({})
+ expect(global.variables_value).to eq({})
end
end
- describe '#stages' do
+ describe '#stages_value' do
it 'returns an array of default stages' do
- expect(global.stages).to eq %w[build test deploy]
+ expect(global.stages_value).to eq %w[build test deploy]
end
end
- describe '#cache' do
+ describe '#cache_value' do
it 'returns correct cache definition' do
- expect(global.cache).to eq(key: 'a')
+ expect(global.cache_value).to eq(key: 'a')
end
end
end
@@ -217,9 +217,9 @@ describe Gitlab::Ci::Config::Node::Global do
{ variables: nil, rspec: { script: 'rspec' } }
end
- describe '#variables' do
+ describe '#variables_value' do
it 'undefined entry returns a default value' do
- expect(global.variables).to eq({})
+ expect(global.variables_value).to eq({})
end
end
end
@@ -245,9 +245,9 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- describe '#before_script' do
+ describe '#before_script_value' do
it 'returns nil' do
- expect(global.before_script).to be_nil
+ expect(global.before_script_value).to be_nil
end
end
end
@@ -284,7 +284,7 @@ describe Gitlab::Ci::Config::Node::Global do
context 'when node exists' do
it 'returns correct entry' do
expect(global[:cache])
- .to be_an_instance_of Gitlab::Ci::Config::Node::Cache
+ .to be_an_instance_of Gitlab::Ci::Config::Entry::Cache
expect(global[:jobs][:rspec][:script].value).to eq ['ls']
end
end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_spec.rb b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb
index 61e2a554419..459362761e6 100644
--- a/spec/lib/gitlab/ci/config/node/hidden_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/hidden_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Hidden do
+describe Gitlab::Ci::Config::Entry::Hidden do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index d11bb39f328..3c99cb0a1ee 100644
--- a/spec/lib/gitlab/ci/config/node/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Image do
+describe Gitlab::Ci::Config::Entry::Image do
let(:entry) { described_class.new(config) }
describe 'validation' do
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 91f676dae03..fc9b8b86dc4 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Job do
+describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
describe 'validations' do
@@ -19,8 +19,7 @@ describe Gitlab::Ci::Config::Node::Job do
let(:entry) { described_class.new(config, name: ''.to_sym) }
it 'reports error' do
- expect(entry.errors)
- .to include "job name can't be blank"
+ expect(entry.errors).to include "job name can't be blank"
end
end
end
@@ -56,6 +55,15 @@ describe Gitlab::Ci::Config::Node::Job do
end
end
end
+
+ context 'when script is not provided' do
+ let(:config) { { stage: 'test' } }
+
+ it 'returns error about missing script entry' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include "job script can't be blank"
+ end
+ end
end
end
@@ -78,7 +86,7 @@ describe Gitlab::Ci::Config::Node::Job do
before { entry.compose!(deps) }
let(:config) do
- { image: 'some_image', cache: { key: 'test' } }
+ { script: 'rspec', image: 'some_image', cache: { key: 'test' } }
end
it 'overrides global config' do
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index 929809339ef..aaebf783962 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Jobs do
+describe Gitlab::Ci::Config::Entry::Jobs do
let(:entry) { described_class.new(config) }
describe 'validations' do
@@ -74,9 +74,9 @@ describe Gitlab::Ci::Config::Node::Jobs do
it 'creates valid descendant nodes' do
expect(entry.descendants.count).to eq 3
expect(entry.descendants.first(2))
- .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
+ .to all(be_an_instance_of(Gitlab::Ci::Config::Entry::Job))
expect(entry.descendants.last)
- .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
+ .to be_an_instance_of(Gitlab::Ci::Config::Entry::Hidden)
end
end
diff --git a/spec/lib/gitlab/ci/config/node/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index 8cda43173fe..a55e5b4b8ac 100644
--- a/spec/lib/gitlab/ci/config/node/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Key do
+describe Gitlab::Ci::Config::Entry::Key do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/paths_spec.rb b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
index 6fd744b3975..e60c9aaf661 100644
--- a/spec/lib/gitlab/ci/config/node/paths_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/paths_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Paths do
+describe Gitlab::Ci::Config::Entry::Paths do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/entry/script_spec.rb
index 219a7e981d3..aa99cee2690 100644
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/script_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Script do
+describe Gitlab::Ci::Config::Entry::Script do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
index be0fe46befd..66fad3b6b16 100644
--- a/spec/lib/gitlab/ci/config/node/services_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Services do
+describe Gitlab::Ci::Config::Entry::Services do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/entry/stage_spec.rb
index fb9ec70762a..70c8a0a355a 100644
--- a/spec/lib/gitlab/ci/config/node/stage_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/stage_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Stage do
+describe Gitlab::Ci::Config::Entry::Stage do
let(:stage) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/stages_spec.rb b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
index 1a3818d8997..182c8d867c7 100644
--- a/spec/lib/gitlab/ci/config/node/stages_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/stages_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Stages do
+describe Gitlab::Ci::Config::Entry::Stages do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
index a4a3e36754e..e4ee44f1274 100644
--- a/spec/lib/gitlab/ci/config/node/trigger_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Trigger do
+describe Gitlab::Ci::Config::Entry::Trigger do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
index 6bde8602963..fdf48d84192 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Undefined do
+describe Gitlab::Ci::Config::Entry::Undefined do
let(:entry) { described_class.new }
describe '#leaf?' do
diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
index ba3ceef24ce..66f88fa35b6 100644
--- a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Unspecified do
+describe Gitlab::Ci::Config::Entry::Unspecified do
let(:unspecified) { described_class.new(entry) }
let(:entry) { spy('Entry') }
diff --git a/spec/lib/gitlab/ci/config/node/validatable_spec.rb b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
index 64b77fd6e03..d1856801827 100644
--- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Validatable do
- let(:node) { Class.new }
+describe Gitlab::Ci::Config::Entry::Validatable do
+ let(:entry) { Class.new }
before do
- node.include(described_class)
+ entry.include(described_class)
end
describe '.validator' do
before do
- node.class_eval do
+ entry.class_eval do
attr_accessor :test_attribute
validations do
@@ -19,34 +19,34 @@ describe Gitlab::Ci::Config::Node::Validatable do
end
it 'returns validator' do
- expect(node.validator.superclass)
- .to be Gitlab::Ci::Config::Node::Validator
+ expect(entry.validator.superclass)
+ .to be Gitlab::Ci::Config::Entry::Validator
end
it 'returns only one validator to mitigate leaks' do
- expect { node.validator }.not_to change { node.validator }
+ expect { entry.validator }.not_to change { entry.validator }
end
- context 'when validating node instance' do
- let(:node_instance) { node.new }
+ context 'when validating entry instance' do
+ let(:entry_instance) { entry.new }
context 'when attribute is valid' do
before do
- node_instance.test_attribute = 'valid'
+ entry_instance.test_attribute = 'valid'
end
it 'instance of validator is valid' do
- expect(node.validator.new(node_instance)).to be_valid
+ expect(entry.validator.new(entry_instance)).to be_valid
end
end
context 'when attribute is not valid' do
before do
- node_instance.test_attribute = nil
+ entry_instance.test_attribute = nil
end
it 'instance of validator is invalid' do
- expect(node.validator.new(node_instance)).to be_invalid
+ expect(entry.validator.new(entry_instance)).to be_invalid
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/validator_spec.rb b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
index 090fd63b844..ad7e6f07d3c 100644
--- a/spec/lib/gitlab/ci/config/node/validator_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Validator do
+describe Gitlab::Ci::Config::Entry::Validator do
let(:validator) { Class.new(described_class) }
let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') }
diff --git a/spec/lib/gitlab/ci/config/node/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
index 4b6d971ec71..58327d08904 100644
--- a/spec/lib/gitlab/ci/config/node/variables_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::Variables do
+describe Gitlab::Ci::Config::Entry::Variables do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/node/configurable_spec.rb b/spec/lib/gitlab/ci/config/node/configurable_spec.rb
deleted file mode 100644
index c468ecf957b..00000000000
--- a/spec/lib/gitlab/ci/config/node/configurable_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Configurable do
- let(:node) { Class.new }
-
- before do
- node.include(described_class)
- end
-
- describe 'validations' do
- let(:validator) { node.validator.new(instance) }
-
- before do
- node.class_eval do
- attr_reader :config
-
- def initialize(config)
- @config = config
- end
- end
-
- validator.validate
- end
-
- context 'when node validator is invalid' do
- let(:instance) { node.new('ls') }
-
- it 'returns invalid validator' do
- expect(validator).to be_invalid
- end
- end
-
- context 'when node instance is valid' do
- let(:instance) { node.new(key: 'value') }
-
- it 'returns valid validator' do
- expect(validator).to be_valid
- end
- end
- end
-
- describe 'configured nodes' do
- before do
- node.class_eval do
- node :object, Object, description: 'test object'
- end
- end
-
- describe '.nodes' do
- it 'has valid nodes' do
- expect(node.nodes).to include :object
- end
-
- it 'creates a node factory' do
- expect(node.nodes[:object])
- .to be_an_instance_of Gitlab::Ci::Config::Node::Factory
- end
-
- it 'returns a duplicated factory object' do
- first_factory = node.nodes[:object]
- second_factory = node.nodes[:object]
-
- expect(first_factory).not_to be_equal(second_factory)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index de3f64249a2..1bbaca0739a 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -257,8 +257,9 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
context 'with an external issue tracker reference' do
it 'extracts the referenced issue' do
jira_project = create(:jira_project, name: 'JIRA_EXT1')
+ jira_project.team << [jira_project.creator, :master]
jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project)
- closing_issue_extractor = described_class.new jira_project
+ closing_issue_extractor = described_class.new(jira_project, jira_project.creator)
message = "Resolve #{jira_issue.to_reference}"
expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue])
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
new file mode 100644
index 00000000000..01b2a55b63c
--- /dev/null
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::ContributionsCalendar do
+ let(:contributor) { create(:user) }
+ let(:user) { create(:user) }
+
+ let(:private_project) do
+ create(:empty_project, :private) do |project|
+ create(:project_member, user: contributor, project: project)
+ end
+ end
+
+ let(:public_project) do
+ create(:empty_project, :public) do |project|
+ create(:project_member, user: contributor, project: project)
+ end
+ end
+
+ let(:feature_project) do
+ create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+ create(:project_member, user: contributor, project: project).project
+ end
+ end
+
+ let(:today) { Time.now.to_date }
+ let(:last_week) { today - 7.days }
+ let(:last_year) { today - 1.year }
+
+ before do
+ travel_to today
+ end
+
+ after do
+ travel_back
+ end
+
+ def calendar(current_user = nil)
+ described_class.new(contributor, current_user)
+ end
+
+ def create_event(project, day)
+ @targets ||= {}
+ @targets[project] ||= create(:issue, project: project, author: contributor)
+
+ Event.create!(
+ project: project,
+ action: Event::CREATED,
+ target: @targets[project],
+ author: contributor,
+ created_at: day,
+ )
+ end
+
+ describe '#activity_dates' do
+ it "returns a hash of date => count" do
+ create_event(public_project, last_week)
+ create_event(public_project, last_week)
+ create_event(public_project, today)
+
+ expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
+ end
+
+ it "only shows private events to authorized users" do
+ create_event(private_project, today)
+ create_event(feature_project, today)
+
+ expect(calendar.activity_dates[today]).to eq(0)
+ expect(calendar(user).activity_dates[today]).to eq(0)
+ expect(calendar(contributor).activity_dates[today]).to eq(2)
+ end
+ end
+
+ describe '#events_by_date' do
+ it "returns all events for a given date" do
+ e1 = create_event(public_project, today)
+ e2 = create_event(public_project, today)
+ create_event(public_project, last_week)
+
+ expect(calendar.events_by_date(today)).to contain_exactly(e1, e2)
+ end
+
+ it "only shows private events to authorized users" do
+ e1 = create_event(public_project, today)
+ e2 = create_event(private_project, today)
+ e3 = create_event(feature_project, today)
+ create_event(public_project, last_week)
+
+ expect(calendar.events_by_date(today)).to contain_exactly(e1)
+ expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
+ end
+ end
+
+ describe '#starting_year' do
+ it "should be the start of last year" do
+ expect(calendar.starting_year).to eq(last_year.year)
+ end
+ end
+
+ describe '#starting_month' do
+ it "should be the start of this month" do
+ expect(calendar.starting_month).to eq(today.month)
+ 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
new file mode 100644
index 00000000000..43f42d1bde8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb
@@ -0,0 +1,10 @@
+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/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
new file mode 100644
index 00000000000..9aeaa6b3ee8
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -0,0 +1,326 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::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 }) }
+
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
+
+ setup(context)
+ end
+
+ describe '#issue_events' do
+ it 'has the total time' do
+ expect(subject.issue_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.issue_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.issue_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.issue_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.issue_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)
+ end
+ end
+
+ describe '#plan_events' do
+ it 'has a title' do
+ expect(subject.plan_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
+ end
+
+ it 'has the URL' do
+ expect(subject.plan_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.plan_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
+ end
+
+ it "has the author's name" do
+ expect(subject.plan_events.first[:author][:name]).not_to be_nil
+ end
+ end
+
+ describe '#code_events' do
+ 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
+ end
+
+ it 'has a title' do
+ expect(subject.code_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.code_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.code_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)
+ end
+ end
+
+ describe '#test_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+ end
+
+ it 'has the name' do
+ expect(subject.test_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.test_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.test_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.test_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
+ end
+
+ it 'has the short SHA' do
+ expect(subject.test_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
+ end
+
+ it 'has the date' do
+ expect(subject.test_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
+ end
+ end
+
+ describe '#review_events' do
+ 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
+ end
+
+ it 'has a title' do
+ expect(subject.review_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)
+ end
+
+ it 'has the URL' do
+ expect(subject.review_events.first[:url]).not_to be_nil
+ end
+
+ it 'has a state' do
+ expect(subject.review_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.review_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
+ end
+
+ it "has the author's name" do
+ expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#staging_events' do
+ let(:merge_request) { MergeRequest.first }
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: context.project)
+ end
+
+ before do
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the name' do
+ expect(subject.staging_events.first[:name]).not_to be_nil
+ end
+
+ it 'has the ID' do
+ expect(subject.staging_events.first[:id]).not_to be_nil
+ end
+
+ it 'has the URL' do
+ expect(subject.staging_events.first[:url]).not_to be_nil
+ end
+
+ it 'has the branch name' do
+ expect(subject.staging_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
+ end
+
+ it 'has the short SHA' do
+ expect(subject.staging_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
+ end
+
+ it 'has the date' do
+ expect(subject.staging_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
+ end
+
+ it "has the author's URL" do
+ expect(subject.staging_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
+ end
+
+ it "has the author's name" do
+ expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
+ end
+ end
+
+ describe '#production_events' do
+ let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ before do
+ merge_merge_requests_closing_issue(context)
+ deploy_master
+ end
+
+ it 'has the total time' do
+ expect(subject.production_events.first[:total_time]).not_to be_empty
+ end
+
+ it 'has a title' do
+ expect(subject.production_events.first[:title]).to eq(context.title)
+ end
+
+ it 'has the URL' do
+ expect(subject.production_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)
+ end
+
+ it 'has a created_at timestamp' do
+ expect(subject.production_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
+ end
+
+ it "has the author's avatar URL" do
+ expect(subject.production_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)
+ end
+ end
+
+ def setup(context)
+ milestone = create(:milestone, project: project)
+ context.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(context)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha)
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
new file mode 100644
index 00000000000..1c5c308da7d
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb
@@ -0,0 +1,10 @@
+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/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
new file mode 100644
index 00000000000..d76a255acf5
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb
@@ -0,0 +1,10 @@
+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
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
new file mode 100644
index 00000000000..ac17e3b4287
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb
@@ -0,0 +1,10 @@
+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/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
new file mode 100644
index 00000000000..1ff53aa0227
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb
@@ -0,0 +1,10 @@
+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/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
new file mode 100644
index 00000000000..7019e4c3351
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb
@@ -0,0 +1,21 @@
+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
+
+ 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
+end
diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
new file mode 100644
index 00000000000..4862d4765f2
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb
@@ -0,0 +1,10 @@
+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/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
new file mode 100644
index 00000000000..e249db69fc6
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb
@@ -0,0 +1,10 @@
+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/updater_spec.rb b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
new file mode 100644
index 00000000000..eff54cd3692
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/updater_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Updater do
+ describe 'updates authors' do
+ let(:user) { create(:user) }
+ let(:events) { [{ 'author_id' => user.id }] }
+
+ it 'maps the correct user' do
+ described_class.update!(events, from: 'author_id', to: 'author', klass: User)
+
+ expect(events.first['author']).to eq(user)
+ end
+ end
+
+ describe 'updates builds' do
+ let(:build) { create(:ci_build) }
+ let(:events) { [{ 'id' => build.id }] }
+
+ it 'maps the correct build' do
+ described_class.update!(events, from: 'id', to: 'build', klass: ::Ci::Build)
+
+ expect(events.first['build']).to eq(build)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0650cb291e5..38475792d93 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -46,4 +46,28 @@ describe Gitlab::Diff::File, lib: true do
expect(diff_file.collapsed?).to eq(false)
end
end
+
+ describe '#old_content_commit' do
+ it 'returns base commit' do
+ old_content_commit = diff_file.old_content_commit
+
+ expect(old_content_commit.id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ end
+ end
+
+ describe '#old_blob' do
+ it 'returns blob of commit of base commit' do
+ old_data = diff_file.old_blob.data
+
+ expect(old_data).to include('raise "System commands must be given as an array of strings"')
+ end
+ end
+
+ describe '#blob' do
+ it 'returns blob of new commit' do
+ data = diff_file.blob.data
+
+ expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"')
+ end
+ end
end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index a5cc7b02936..cb3651e3845 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require_relative '../email_shared_blocks'
-xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
@@ -18,7 +18,7 @@ xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
create(
:user,
email: 'jake@adventuretime.ooo',
- authentication_token: 'auth_token'
+ incoming_email_token: 'auth_token'
)
end
@@ -60,8 +60,8 @@ xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
end
end
- context "when we can't find the authentication_token" do
- let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") }
+ context "when we can't find the incoming_email_token" do
+ let(:email_raw) { fixture_file("emails/wrong_incoming_email_token.eml") }
it "raises an UserNotFoundError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 6f8e9a4be64..c7a0139d32a 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -206,5 +206,9 @@ describe Gitlab::Email::ReplyParser, lib: true do
it "properly renders email reply from MS Outlook client" do
expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010")
end
+
+ it "properly renders html-only email from MS Outlook" do
+ expect(test_parse_body(fixture_file("emails/outlook_html.eml"))).to eq("Microsoft Outlook 2010")
+ end
end
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index f045463c1cb..6b3dfebd85d 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
let(:new_project) { create(:project, name: 'new') }
let(:user) { create(:user) }
- before { old_project.team << [user, :guest] }
+ before { old_project.team << [user, :reporter] }
describe '#rewrite' do
subject do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 62aa212f1f6..f1d0a190002 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -66,6 +66,7 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
+ it { expect(subject.message).to match(/You are not allowed to download code/) }
end
end
@@ -77,6 +78,7 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
+ it { expect(subject.message).to match(/Your account has been blocked/) }
end
end
@@ -84,6 +86,29 @@ describe Gitlab::GitAccess, lib: true do
context 'pull code' do
it { expect(subject.allowed?).to be_falsey }
end
+
+ context 'when project is public' do
+ let(:public_project) { create(:project, :public) }
+ let(:guest_access) { Gitlab::GitAccess.new(nil, public_project, 'web', authentication_abilities: []) }
+ subject { guest_access.check('git-upload-pack', '_any') }
+
+ context 'when repository is enabled' do
+ it 'give access to download code' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+ expect(subject.allowed?).to be_truthy
+ end
+ end
+
+ context 'when repository is disabled' do
+ it 'does not give access to download code' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ expect(subject.allowed?).to be_falsey
+ expect(subject.message).to match(/You are not allowed to download code/)
+ end
+ end
+ end
end
describe 'deploy key permissions' do
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 576cda595bb..576aa5c366f 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::GitAccessWiki, lib: true do
project.team << [user, :developer]
end
- subject { access.push_access_check(changes) }
+ subject { access.check('git-receive-pack', changes) }
it { expect(subject.allowed?).to be_truthy }
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 7478f86bd28..000b9aa6f83 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -101,7 +101,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
closed_at: nil,
merged_at: nil,
url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
- labels: [double(name: 'Label #3')],
)
end
@@ -157,8 +156,6 @@ describe Gitlab::GithubImport::Importer, lib: true do
errors: [
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
]
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
index c2f1f6b91a1..95339e2f128 100644
--- a/spec/lib/gitlab/github_import/issue_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -144,20 +144,20 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
end
end
- describe '#valid?' do
+ describe '#pull_request?' do
context 'when mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: double)) }
- it 'returns false' do
- expect(issue.valid?).to eq false
+ it 'returns true' do
+ expect(issue.pull_request?).to eq true
end
end
context 'when does not mention a pull request' do
let(:raw_data) { double(base_data.merge(pull_request: nil)) }
- it 'returns true' do
- expect(issue.valid?).to eq true
+ it 'returns false' do
+ expect(issue.pull_request?).to eq false
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 02b11bd999a..fe3c39e38db 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -116,6 +116,7 @@ project:
- base_tags
- tag_taggings
- tags
+- chat_services
- creator
- group
- namespace
@@ -127,6 +128,7 @@ project:
- emails_on_push_service
- builds_email_service
- pipelines_email_service
+- mattermost_slash_commands_service
- irker_service
- pivotaltracker_service
- hipchat_service
@@ -188,4 +190,4 @@ award_emoji:
- awardable
- user
priorities:
-- label \ No newline at end of file
+- label
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 07a2c316899..d6807941b31 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -258,6 +258,7 @@ Service:
- template
- push_events
- issues_events
+- commit_events
- merge_requests_events
- tag_push_events
- note_events
@@ -339,4 +340,4 @@ LabelPriority:
- label_id
- priority
- created_at
-- updated_at \ No newline at end of file
+- updated_at
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 835853a83a4..1a6803e01c3 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -1,20 +1,132 @@
require 'spec_helper'
describe Gitlab::LDAP::Config, lib: true do
- let(:config) { Gitlab::LDAP::Config.new provider }
- let(:provider) { 'ldapmain' }
+ include LdapHelpers
+
+ let(:config) { Gitlab::LDAP::Config.new('ldapmain') }
describe '#initalize' do
it 'requires a provider' do
expect{ Gitlab::LDAP::Config.new }.to raise_error ArgumentError
end
- it "works" do
+ it 'works' do
expect(config).to be_a described_class
end
- it "raises an error if a unknow provider is used" do
+ it 'raises an error if a unknown provider is used' do
expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error(RuntimeError)
end
end
+
+ describe '#adapter_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'method' => 'plain'
+ }
+ )
+
+ expect(config.adapter_options).to eq(
+ host: 'ldap.example.com',
+ port: 386,
+ encryption: nil
+ )
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'method' => 'ssl',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.adapter_options).to eq(
+ host: 'ldap.example.com',
+ port: 686,
+ encryption: :simple_tls,
+ auth: {
+ method: :simple,
+ username: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ }
+ )
+ end
+ end
+
+ describe '#omniauth_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'base' => 'ou=users,dc=example,dc=com',
+ 'method' => 'plain',
+ 'uid' => 'uid'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ host: 'ldap.example.com',
+ port: 386,
+ base: 'ou=users,dc=example,dc=com',
+ method: 'plain',
+ filter: '(uid=%{username})'
+ )
+ expect(config.omniauth_options.keys).not_to include(:bind_dn, :password)
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'uid' => 'sAMAccountName',
+ 'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ filter: '(&(sAMAccountName=%{username})(memberOf=cn=group1,ou=groups,dc=example,dc=com))',
+ bind_dn: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ )
+ end
+ end
+
+ describe '#has_auth?' do
+ it 'is true when password is set' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is true when bind_dn is set and password is empty' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => ''
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is false when password and bind_dn are not set' do
+ stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil })
+
+ expect(config.has_auth?).to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 117a15264da..fd3769d75b5 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::Go, lib: true do
resp = middleware.call(env)
expect(resp[0]).to eq(200)
expect(resp[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n"
+ expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n"
expect(resp[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 78c669e8fa5..fc9e1cb430a 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -137,11 +137,12 @@ describe Gitlab::OAuth::User, lib: true do
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' }
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context "and no account for the LDAP user" do
it "creates a user with dual LDAP and omniauth identities" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -159,6 +160,8 @@ describe Gitlab::OAuth::User, lib: true do
context "and LDAP user has an account already" do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
oauth_user.save
expect(gl_user).to be_valid
@@ -172,6 +175,24 @@ describe Gitlab::OAuth::User, lib: true do
])
end
end
+
+ context 'when an LDAP person is not found by uid' do
+ it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+ allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+
+ oauth_user.save
+
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash)
+ .to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
end
context "and no corresponding LDAP person" do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 29abb4d4d07..a0fdad87eee 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -6,22 +6,65 @@ describe Gitlab::ProjectSearchResults, lib: true do
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
+ let(:results) { described_class.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
- it { expect(results.repository_ref).to be_nil }
it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
+ let(:results) { described_class.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+ describe 'blob search' do
+ let(:results) { described_class.new(user, project, 'files').objects('blobs') }
+
+ it 'finds by name' do
+ expect(results).to include(["files/images/wm.svg", nil])
+ end
+
+ it 'finds by content' do
+ blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+
+ expect(blob.filename).to eq("CHANGELOG")
+ end
+
+ describe 'parsing results' do
+ let(:results) { project.repository.search_files_by_content('feature', 'master') }
+ let(:search_result) { results.first }
+
+ subject { described_class.parse_search_result(search_result) }
+
+ it "returns a valid OpenStruct object" do
+ is_expected.to be_an OpenStruct
+ expect(subject.filename).to eq('CHANGELOG')
+ expect(subject.basename).to eq('CHANGELOG')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(188)
+ expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n")
+ end
+
+ context "when filename has extension" do
+ let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+
+ it { expect(subject.filename).to eq('CONTRIBUTE.md') }
+ it { expect(subject.basename).to eq('CONTRIBUTE') }
+ end
+
+ context "when file under directory" do
+ let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+
+ it { expect(subject.filename).to eq('a/b/c.md') }
+ it { expect(subject.basename).to eq('a/b/c') }
+ end
+ end
+ end
+
describe 'confidential issues' do
let(:query) { 'issue' }
let(:author) { create(:user) }
@@ -66,7 +109,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it 'lists project confidential issues for assignee' do
- results = described_class.new(assignee, project.id, query)
+ results = described_class.new(assignee, project, query)
issues = results.objects('issues')
expect(issues).to include issue
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7b4ccc83915..bf0ab9635fd 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
+ before { project.team << [project.creator, :developer] }
+
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
@@ -42,7 +44,6 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
- project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb
new file mode 100644
index 00000000000..ff32e0e699d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqThrottler do
+ before do
+ Sidekiq.options[:concurrency] = 35
+
+ stub_application_setting(
+ sidekiq_throttling_enabled: true,
+ sidekiq_throttling_factor: 0.1,
+ sidekiq_throttling_queues: %w[build project_cache]
+ )
+ end
+
+ describe '#execute!' do
+ it 'sets limits on the selected queues' do
+ Gitlab::SidekiqThrottler.execute!
+
+ expect(Sidekiq::Queue['build'].limit).to eq 4
+ expect(Sidekiq::Queue['project_cache'].limit).to eq 4
+ end
+
+ it 'does not set limits on other queues' do
+ Gitlab::SidekiqThrottler.execute!
+
+ expect(Sidekiq::Queue['merge'].limit).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/light_url_builder_spec.rb b/spec/lib/light_url_builder_spec.rb
new file mode 100644
index 00000000000..a826b24419a
--- /dev/null
+++ b/spec/lib/light_url_builder_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBuilder, lib: true do
+ describe '.build' do
+ context 'when passing a Commit' do
+ it 'returns a proper URL' do
+ commit = build_stubbed(:commit)
+
+ url = described_class.build(commit)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{commit.project.path_with_namespace}/commit/#{commit.id}"
+ end
+ end
+
+ context 'when passing an Issue' do
+ it 'returns a proper URL' do
+ issue = build_stubbed(:issue, iid: 42)
+
+ url = described_class.build(issue)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}"
+ end
+ end
+
+ context 'when passing a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = build_stubbed(:merge_request, iid: 42)
+
+ url = described_class.build(merge_request)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}"
+ end
+ end
+
+ context 'when passing a Note' do
+ context 'on a Commit' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on a Commit Diff' do
+ it 'returns a proper URL' do
+ note = build_stubbed(:diff_note_on_commit)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{note.project.path_with_namespace}/commit/#{note.commit_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on an Issue' do
+ it 'returns a proper URL' do
+ issue = create(:issue, iid: 42)
+ note = build_stubbed(:note_on_issue, noteable: issue)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{issue.project.path_with_namespace}/issues/#{issue.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a MergeRequest Diff' do
+ it 'returns a proper URL' do
+ merge_request = create(:merge_request, iid: 42)
+ note = build_stubbed(:diff_note_on_merge_request, noteable: merge_request)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{merge_request.project.path_with_namespace}/merge_requests/#{merge_request.iid}#note_#{note.id}"
+ end
+ end
+
+ context 'on a ProjectSnippet' do
+ it 'returns a proper URL' do
+ project_snippet = create(:project_snippet)
+ note = build_stubbed(:note_on_project_snippet, noteable: project_snippet)
+
+ url = described_class.build(note)
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{project_snippet.project.path_with_namespace}/snippets/#{note.noteable_id}#note_#{note.id}"
+ end
+ end
+
+ context 'on another object' do
+ it 'returns a proper URL' do
+ project = build_stubbed(:project)
+
+ expect { described_class.build(project) }.
+ to raise_error(NotImplementedError, 'No URL builder defined for Project')
+ end
+ end
+ end
+
+ context 'when passing a WikiPage' do
+ it 'returns a proper URL' do
+ wiki_page = build(:wiki_page)
+ url = described_class.build(wiki_page)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
+ end
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 14bc062ef12..e1877d5fde0 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -25,7 +25,7 @@ describe Notify do
it 'includes a link for user to set password' do
params = "reset_password_token=#{token}"
is_expected.to have_body_text(
- %r{http://localhost(:\d+)?/users/password/edit\?#{params}}
+ %r{http://#{Gitlab.config.gitlab.host}(:\d+)?/users/password/edit\?#{params}}
)
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f5f3f58613d..932a5dc4862 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -401,7 +401,12 @@ describe Notify do
describe 'project access requested' do
context 'for a project in a user namespace' do
- let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable) do |project|
+ project.team << [project.owner, :master, project.owner]
+ end
+ end
+
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -428,7 +433,7 @@ describe Notify do
context 'for a project in a group' do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: group) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -454,7 +459,7 @@ describe Notify do
end
describe 'project access denied' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -474,7 +479,7 @@ describe Notify do
end
describe 'project access changed' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -685,7 +690,7 @@ describe Notify do
context 'for a group' do
describe 'group access requested' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 02d6263094a..219db365a91 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- subject { create(:broadcast_message) }
+ subject { build(:broadcast_message) }
it { is_expected.to be_valid }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ae185de9ca3..ef07f2275b1 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1052,4 +1052,132 @@ describe Ci::Build, models: true do
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 variables' do
+ let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') }
+
+ it { is_expected.to eq('review/master') }
+ end
+ end
end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
new file mode 100644
index 00000000000..b02971cab82
--- /dev/null
+++ b/spec/models/chat_name_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe ChatName, models: true do
+ subject { create(:chat_name) }
+
+ it { is_expected.to belong_to(:service) }
+ it { is_expected.to belong_to(:user) }
+
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:service) }
+ it { is_expected.to validate_presence_of(:team_id) }
+ it { is_expected.to validate_presence_of(:chat_id) }
+
+ it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) }
+ it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a37a00f461a..a7e90c8a381 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -4,6 +4,12 @@ describe Ci::Build, models: true do
let(:build) { create(:ci_build) }
let(:test_trace) { 'This is a test' }
+ 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) }
+
describe '#trace' do
it 'obfuscates project runners token' do
allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5eb14dc6bd2..71b7628ef10 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -524,4 +524,78 @@ describe Ci::Pipeline, models: true do
expect(pipeline.merge_requests).to be_empty
end
end
+
+ describe 'notifications when pipeline success or failed' do
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: create(:user))
+ end
+
+ before do
+ reset_delivered_emails!
+
+ project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+
+ perform_enqueued_jobs do
+ pipeline.enqueue
+ pipeline.run
+ end
+ end
+
+ shared_examples 'sending a notification' do
+ it 'sends an email' do
+ should_only_email(pipeline.user, kind: :bcc)
+ end
+ end
+
+ shared_examples 'not sending any notification' do
+ it 'does not send any email' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'with success pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.succeed
+ end
+ end
+
+ it_behaves_like 'sending a notification'
+ end
+
+ context 'with failed pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.drop
+ end
+ end
+
+ it_behaves_like 'sending a notification'
+ end
+
+ context 'with skipped pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.skip
+ end
+ end
+
+ it_behaves_like 'not sending any notification'
+ end
+
+ context 'with cancelled pipeline' do
+ before do
+ perform_enqueued_jobs do
+ pipeline.cancel
+ end
+ end
+
+ it_behaves_like 'not sending any notification'
+ end
+ end
end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index 96eee0e8bdd..4829ef17a20 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe AccessRequestable do
describe 'Group' do
describe '#request_access' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(group.request_access(user)).to be_a(GroupMember) }
@@ -11,7 +11,7 @@ describe AccessRequestable do
end
describe '#access_requested?' do
- let(:group) { create(:group, :public) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
before { group.request_access(user) }
@@ -22,14 +22,14 @@ describe AccessRequestable do
describe 'Project' do
describe '#request_access' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
it { expect(project.request_access(user)).to be_a(ProjectMember) }
end
describe '#access_requested?' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
before { project.request_access(user) }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index a59d30687f6..6f84bffe046 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -97,6 +97,11 @@ describe Issue, "Issuable" do
end
end
+ describe '.to_ability_name' do
+ it { expect(Issue.to_ability_name).to eq("issue") }
+ it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
+ end
+
describe "#today?" do
it "returns true when created today" do
# Avoid timezone differences and just return exactly what we want
@@ -171,23 +176,25 @@ describe Issue, "Issuable" do
end
describe '#subscribed?' do
+ let(:project) { issue.project }
+
context 'user is not a participant in the issue' do
before { allow(issue).to receive(:participants).with(user).and_return([]) }
it 'returns false when no subcription exists' do
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
it 'returns true when a subcription exists and subscribed is true' do
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
- issue.subscriptions.create(user: user, subscribed: false)
+ issue.subscriptions.create(user: user, project: project, subscribed: false)
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
end
@@ -195,19 +202,19 @@ describe Issue, "Issuable" do
before { allow(issue).to receive(:participants).with(user).and_return([user]) }
it 'returns false when no subcription exists' do
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns true when a subcription exists and subscribed is true' do
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
- expect(issue.subscribed?(user)).to be_truthy
+ expect(issue.subscribed?(user, project)).to be_truthy
end
it 'returns false when a subcription exists and subscribed is false' do
- issue.subscriptions.create(user: user, subscribed: false)
+ issue.subscriptions.create(user: user, project: project, subscribed: false)
- expect(issue.subscribed?(user)).to be_falsey
+ expect(issue.subscribed?(user, project)).to be_falsey
end
end
end
@@ -341,4 +348,25 @@ describe Issue, "Issuable" do
expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2])
end
end
+
+ describe '#assignee_or_author?' do
+ let(:user) { build(:user, id: 1) }
+ let(:issue) { build(:issue) }
+
+ it 'returns true for a user that is assigned to an issue' do
+ issue.assignee = user
+
+ expect(issue.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns true for a user that is the author of an issue' do
+ issue.author = user
+
+ expect(issue.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns false for a user that is not the assignee or author' do
+ expect(issue.assignee_or_author?(user)).to eq(false)
+ end
+ end
end
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index b7fc5a92497..58f5c164116 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -1,67 +1,128 @@
require 'spec_helper'
describe Subscribable, 'Subscribable' do
- let(:resource) { create(:issue) }
- let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:resource) { create(:issue, project: project) }
+ let(:user_1) { create(:user) }
describe '#subscribed?' do
- it 'returns false when no subcription exists' do
- expect(resource.subscribed?(user)).to be_falsey
- end
+ context 'without project' do
+ it 'returns false when no subscription exists' do
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user_1, subscribed: true)
+
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
- it 'returns true when a subcription exists and subscribed is true' do
- resource.subscriptions.create(user: user, subscribed: true)
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user_1, subscribed: false)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
end
- it 'returns false when a subcription exists and subscribed is false' do
- resource.subscriptions.create(user: user, subscribed: false)
+ context 'with project' do
+ it 'returns false when no subscription exists' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
+
+ it 'returns true when a subcription exists and subscribed is true' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: true)
+
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
- expect(resource.subscribed?(user)).to be_falsey
+ it 'returns false when a subcription exists and subscribed is false' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: false)
+
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
end
end
+
describe '#subscribers' do
it 'returns [] when no subcribers exists' do
- expect(resource.subscribers).to be_empty
+ expect(resource.subscribers(project)).to be_empty
end
it 'returns the subscribed users' do
- resource.subscriptions.create(user: user, subscribed: true)
- resource.subscriptions.create(user: create(:user), subscribed: false)
+ user_2 = create(:user)
+ resource.subscriptions.create(user: user_1, subscribed: true)
+ resource.subscriptions.create(user: user_2, project: project, subscribed: true)
+ resource.subscriptions.create(user: create(:user), project: project, subscribed: false)
- expect(resource.subscribers).to eq [user]
+ expect(resource.subscribers(project)).to contain_exactly(user_1, user_2)
end
end
describe '#toggle_subscription' do
- it 'toggles the current subscription state for the given user' do
- expect(resource.subscribed?(user)).to be_falsey
+ context 'without project' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user_1)).to be_falsey
- resource.toggle_subscription(user)
+ resource.toggle_subscription(user_1)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
+ end
+
+ context 'with project' do
+ it 'toggles the current subscription state for the given user' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+
+ resource.toggle_subscription(user_1, project)
+
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
end
end
describe '#subscribe' do
- it 'subscribes the given user' do
- expect(resource.subscribed?(user)).to be_falsey
+ context 'without project' do
+ it 'subscribes the given user' do
+ expect(resource.subscribed?(user_1)).to be_falsey
+
+ resource.subscribe(user_1)
+
+ expect(resource.subscribed?(user_1)).to be_truthy
+ end
+ end
+
+ context 'with project' do
+ it 'subscribes the given user' do
+ expect(resource.subscribed?(user_1, project)).to be_falsey
- resource.subscribe(user)
+ resource.subscribe(user_1, project)
- expect(resource.subscribed?(user)).to be_truthy
+ expect(resource.subscribed?(user_1, project)).to be_truthy
+ end
end
end
describe '#unsubscribe' do
- it 'unsubscribes the given current user' do
- resource.subscriptions.create(user: user, subscribed: true)
- expect(resource.subscribed?(user)).to be_truthy
+ context 'without project' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user_1, subscribed: true)
+ expect(resource.subscribed?(user_1)).to be_truthy
+
+ resource.unsubscribe(user_1)
+
+ expect(resource.subscribed?(user_1)).to be_falsey
+ end
+ end
+
+ context 'with project' do
+ it 'unsubscribes the given current user' do
+ resource.subscriptions.create(user: user_1, project: project, subscribed: true)
+ expect(resource.subscribed?(user_1, project)).to be_truthy
- resource.unsubscribe(user)
+ resource.unsubscribe(user_1, project)
- expect(resource.subscribed?(user)).to be_falsey
+ expect(resource.subscribed?(user_1, project)).to be_falsey
+ end
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index a94e6d0165f..60bbe3fcd72 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -166,4 +166,25 @@ describe Environment, models: true do
end
end
end
+
+ describe 'recently_updated_on_branch?' do
+ subject { environment.recently_updated_on_branch?('feature') }
+
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it { is_expected.to be false }
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index aca49be2942..b684053cd02 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -27,13 +27,14 @@ describe Event, models: true do
end
describe "Push event" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :private) }
let(:user) { project.owner }
let(:event) { create_event(project, user) }
it do
expect(event.push?).to be_truthy
- expect(event.visible_to_user?).to be_truthy
+ expect(event.visible_to_user?(user)).to be_truthy
+ expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
expect(event.branch_name).to eq("master")
expect(event.author).to eq(user)
@@ -93,6 +94,7 @@ describe Event, models: true do
let(:admin) { create(:admin) }
let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
let(:event) { Event.new(project: project, target: target, author_id: author.id) }
@@ -102,6 +104,32 @@ describe Event, models: true do
project.team << [guest, :guest]
end
+ context 'commit note event' do
+ let(:target) { note_on_commit }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq true
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq true
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+
+ context 'private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it do
+ aggregate_failures do
+ expect(event.visible_to_user?(non_member)).to eq false
+ expect(event.visible_to_user?(member)).to eq true
+ expect(event.visible_to_user?(guest)).to eq false
+ expect(event.visible_to_user?(admin)).to eq true
+ end
+ end
+ end
+ end
+
context 'issue event' do
context 'for non confidential issues' do
let(:target) { issue }
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index ebba6e14578..2debe1289a3 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ExternalIssue, models: true do
- let(:project) { double('project', to_reference: 'namespace1/project1') }
+ let(:project) { double('project', id: 1, to_reference: 'namespace1/project1') }
let(:issue) { described_class.new('EXT-1234', project) }
describe 'modules' do
@@ -36,4 +36,10 @@ describe ExternalIssue, models: true do
end
end
end
+
+ describe '#project_id' do
+ it 'returns the ID of the project' do
+ expect(issue.project_id).to eq(project.id)
+ end
+ end
end
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index 85eb889225b..2369658bf78 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -18,7 +18,7 @@ describe GroupLabel, models: true do
end
describe '#to_reference' do
- let(:label) { create(:group_label) }
+ let(:label) { create(:group_label, title: 'feature') }
context 'using id' do
it 'returns a String reference to the object' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 47f89f744cb..1613a586a2c 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Group, models: true do
- let!(:group) { create(:group) }
+ let!(:group) { create(:group, :access_requestable) }
describe 'associations' do
it { is_expected.to have_many :projects }
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
new file mode 100644
index 00000000000..d79f929f7a1
--- /dev/null
+++ b/spec/models/guest_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Guest, lib: true do
+ let(:public_project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:internal_project) { create(:project, :internal) }
+
+ describe '.can_pull?' do
+ context 'when project is private' do
+ it 'does not allow to pull the repo' do
+ expect(Guest.can?(:download_code, private_project)).to eq(false)
+ end
+ end
+
+ context 'when project is internal' do
+ it 'does not allow to pull the repo' do
+ expect(Guest.can?(:download_code, internal_project)).to eq(false)
+ end
+ end
+
+ context 'when project is public' do
+ context 'when repository is disabled' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is accessible only by team members' do
+ it 'does not allow to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::PRIVATE)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(false)
+ end
+ end
+
+ context 'when repository is enabled' do
+ it 'allows to pull the repo' do
+ public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED)
+
+ expect(Guest.can?(:download_code, public_project)).to eq(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
new file mode 100644
index 00000000000..d742c814680
--- /dev/null
+++ b/spec/models/issue_collection_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe IssueCollection do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project) }
+ let(:collection) { described_class.new([issue1, issue2]) }
+
+ describe '#collection' do
+ it 'returns the issues in the same order as the input Array' do
+ expect(collection.collection).to eq([issue1, issue2])
+ end
+ end
+
+ describe '#updatable_by_user' do
+ context 'using an admin user' do
+ it 'returns all issues' do
+ user = create(:admin)
+
+ expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
+ end
+ end
+
+ context 'using a user that has no access to the project' do
+ it 'returns no issues when the user is not an assignee or author' do
+ expect(collection.updatable_by_user(user)).to be_empty
+ end
+
+ it 'returns the issues the user is assigned to' do
+ issue1.assignee = user
+
+ expect(collection.updatable_by_user(user)).to eq([issue1])
+ end
+
+ it 'returns the issues for which the user is the author' do
+ issue1.author = user
+
+ expect(collection.updatable_by_user(user)).to eq([issue1])
+ end
+ end
+
+ context 'using a user that has reporter access to the project' do
+ it 'returns the issues of the project' do
+ project.team << [user, :reporter]
+
+ expect(collection.updatable_by_user(user)).to eq([issue1, issue2])
+ end
+ end
+
+ context 'using a user that is the owner of a project' do
+ it 'returns the issues of the project' do
+ expect(collection.updatable_by_user(project.namespace.owner)).
+ to eq([issue1, issue2])
+ end
+ end
+ end
+
+ describe '#visible_to' do
+ it 'is an alias for updatable_by_user' do
+ updatable_by_user = described_class.instance_method(:updatable_by_user)
+ visible_to = described_class.instance_method(:visible_to)
+
+ expect(visible_to).to eq(updatable_by_user)
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 60d30eb7418..300425767ed 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,7 +22,7 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
- describe 'visible_to_user' do
+ describe '.visible_to_user' do
let(:user) { create(:user) }
let(:authorized_user) { create(:user) }
let(:project) { create(:project, namespace: authorized_user.namespace) }
@@ -102,17 +102,17 @@ describe Issue, models: true do
it 'returns the merge request to close this issue' do
mr
- expect(issue.closed_by_merge_requests).to eq([mr])
+ expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
end
it "returns an empty array when the merge request is closed already" do
closed_mr
- expect(issue.closed_by_merge_requests).to eq([])
+ expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
end
it "returns an empty array when the current issue is closed already" do
- expect(closed_issue.closed_by_merge_requests).to eq([])
+ expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([])
end
end
@@ -218,7 +218,7 @@ describe Issue, models: true do
source_project: subject.project,
source_branch: "#{subject.iid}-branch" })
merge_request.create_cross_references!(user)
- expect(subject.referenced_merge_requests).not_to be_empty
+ expect(subject.referenced_merge_requests(user)).not_to be_empty
expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
@@ -314,6 +314,22 @@ describe Issue, models: true do
end
describe '#visible_to_user?' do
+ context 'without a user' do
+ let(:issue) { build(:issue) }
+
+ it 'returns true when the issue is publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(true)
+
+ expect(issue.visible_to_user?).to eq(true)
+ end
+
+ it 'returns false when the issue is not publicly visible' do
+ expect(issue).to receive(:publicly_visible?).and_return(false)
+
+ expect(issue.visible_to_user?).to eq(false)
+ end
+ end
+
context 'with a user' do
let(:user) { build(:user) }
let(:issue) { build(:issue) }
@@ -329,26 +345,24 @@ describe Issue, models: true do
expect(issue.visible_to_user?(user)).to eq(false)
end
- end
- context 'without a user' do
- let(:issue) { build(:issue) }
+ it 'returns false when feature is disabled' do
+ expect(issue).not_to receive(:readable_by?)
- it 'returns true when the issue is publicly visible' do
- expect(issue).to receive(:publicly_visible?).and_return(true)
+ issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- expect(issue.visible_to_user?).to eq(true)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
- it 'returns false when the issue is not publicly visible' do
- expect(issue).to receive(:publicly_visible?).and_return(false)
+ it 'returns false when restricted for members' do
+ expect(issue).not_to receive(:readable_by?)
- expect(issue.visible_to_user?).to eq(false)
+ issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)
+
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
- end
- describe '#readable_by?' do
describe 'with a regular user that is not a team member' do
let(:user) { create(:user) }
@@ -358,13 +372,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, project: project, confidential: true)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
@@ -375,13 +389,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
@@ -393,13 +407,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
end
end
@@ -410,26 +424,28 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(false)
end
context 'when the user is the project owner' do
+ before { project.team << [user, :master] }
+
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -447,13 +463,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
@@ -467,13 +483,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
@@ -487,13 +503,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -505,13 +521,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
it 'returns true for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).to be_readable_by(user)
+ expect(issue.visible_to_user?(user)).to eq(true)
end
end
end
@@ -523,13 +539,13 @@ describe Issue, models: true do
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).to be_publicly_visible
+ expect(issue).to be_truthy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
@@ -539,13 +555,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
@@ -555,13 +571,13 @@ describe Issue, models: true do
it 'returns false for a regular issue' do
issue = build(:issue, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
it 'returns false for a confidential issue' do
issue = build(:issue, :confidential, project: project)
- expect(issue).not_to be_publicly_visible
+ expect(issue).not_to be_falsy
end
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7fc6ed1dd54..90731f55470 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -19,7 +19,7 @@ describe Key, models: true do
describe "#publishable_keys" do
it 'replaces SSH key comment with simple identifier of username + hostname' do
- expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)")
+ expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end
end
end
@@ -71,15 +71,25 @@ describe Key, models: true do
context 'callbacks' do
it 'adds new key to authorized_file' do
- @key = build(:personal_key, id: 7)
- expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key)
- @key.save
+ key = build(:personal_key, id: 7)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, key.shell_id, key.key)
+ key.save!
end
it 'removes key from authorized_file' do
- @key = create(:personal_key)
- expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key)
- @key.destroy
+ key = create(:personal_key)
+ expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, key.shell_id, key.key)
+ key.destroy
+ end
+ end
+
+ describe '#key=' do
+ let(:valid_key) do
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com"
+ end
+
+ it 'strips white spaces' do
+ expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 485121701af..4f7c8a36cb5 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:empty_project, :public)
+ project = create(:empty_project, :public, :access_requestable)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -174,7 +174,7 @@ describe Member, models: true do
describe '.add_user' do
%w[project group].each do |source_type|
context "when source is a #{source_type}" do
- let!(:source) { create(source_type, :public) }
+ let!(:source) { create(source_type, :public, :access_requestable) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
@@ -443,6 +443,16 @@ describe Member, models: true do
member.accept_invite!(user)
end
+
+ it "refreshes user's authorized projects", truncate: true do
+ project = member.source
+
+ expect(user.authorized_projects).not_to include(project)
+
+ member.accept_invite!(user)
+
+ expect(user.authorized_projects.reload).to include(project)
+ end
end
describe "#decline_invite!" do
@@ -468,4 +478,16 @@ describe Member, models: true do
expect { member.generate_invite_token }.to change { member.invite_token}
end
end
+
+ describe "destroying a record", truncate: true do
+ it "refreshes user's authorized projects" do
+ project = create(:project, :private)
+ user = create(:user)
+ member = project.team << [user, :reporter]
+
+ member.destroy
+
+ expect(user.authorized_projects).not_to include(project)
+ end
+ end
end
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index c5ff1941378..47397a822c1 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -14,4 +14,20 @@ describe ProjectGroupLink do
it { should validate_presence_of(:group) }
it { should validate_presence_of(:group_access) }
end
+
+ describe "destroying a record", truncate: true do
+ it "refreshes group users' authorized projects" do
+ project = create(:project, :private)
+ group = create(:group)
+ reporter = create(:user)
+ group_users = group.users
+
+ group.add_reporter(reporter)
+ project.project_group_links.create(group: group)
+ group_users.each { |user| expect(user.authorized_projects).to include(project) }
+
+ project.project_group_links.destroy_all
+ group_users.each { |user| expect(user.authorized_projects).not_to include(project) }
+ end
+ end
end
diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb
new file mode 100644
index 00000000000..c6a45a3e1be
--- /dev/null
+++ b/spec/models/project_services/chat_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe ChatService, models: true do
+ describe "Associations" do
+ it { is_expected.to have_many :chat_names }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ it 'is false as it has no token' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 652804fb444..9b80f0e7296 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -35,9 +35,9 @@ describe GitlabIssueTrackerService, models: true do
end
it 'gives the correct path' do
- expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
+ expect(@service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(@service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(@service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 05ee4a08391..f5da967cd14 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,6 +69,7 @@ describe JiraService, models: true do
end
describe "Execute" do
+ let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request) }
@@ -82,20 +83,34 @@ describe JiraService, models: true do
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
- project_key: 'GitLabProject'
+ project_key: 'GitLabProject',
+ jira_issue_transition_id: "custom-id"
)
+ # These stubs are needed to test JiraService#close_issue.
+ # We close the issue then do another request to API to check if it got closed.
+ # Here is stubbed the API return with a closed and an opened issues.
+ open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123")
+
@jira_service.save
project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
@project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject'
@transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
+ @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
WebMock.stub_request(:get, @project_url)
WebMock.stub_request(:get, project_issues_url)
WebMock.stub_request(:post, @transitions_url)
WebMock.stub_request(:post, @comment_url)
+ WebMock.stub_request(:post, @remote_link_url)
end
it "calls JIRA API" do
@@ -106,11 +121,44 @@ describe JiraService, models: true do
).once
end
+ # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
+ # for more information
+ it "creates Remote Link reference in JIRA for comment" do
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ # Creates comment
+ expect(WebMock).to have_requested(:post, @comment_url)
+
+ # Creates Remote Link in JIRA issue fields
+ expect(WebMock).to have_requested(:post, @remote_link_url).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}",
+ title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } }
+ }
+ )
+ ).once
+ end
+
+ it "does not send comment or remote links to issues already closed" do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
+
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
+ end
+
it "references the GitLab commit/merge request" do
+ stub_config_setting(base_url: custom_base_url)
+
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+ body: /#{custom_base_url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
).once
end
@@ -130,11 +178,10 @@ describe JiraService, models: true do
end
it "calls the api with jira_issue_transition_id" do
- @jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @transitions_url).with(
- body: /this-is-a-custom-id/
+ body: /custom-id/
).once
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
new file mode 100644
index 00000000000..4a1037e950b
--- /dev/null
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe MattermostSlashCommandsService, models: true do
+ describe "Associations" do
+ it { is_expected.to respond_to :token }
+ end
+
+ describe '#valid_token?' do
+ subject { described_class.new }
+
+ context 'when the token is empty' do
+ it 'is false' do
+ expect(subject.valid_token?('wer')).to be_falsey
+ end
+ end
+
+ context 'when there is a token' do
+ before do
+ subject.token = '123'
+ end
+
+ it 'accepts equal tokens' do
+ expect(subject.valid_token?('123')).to be_truthy
+ end
+ end
+ end
+
+ describe '#trigger' do
+ subject { described_class.new }
+
+ context 'no token is passed' do
+ let(:params) { Hash.new }
+
+ it 'returns nil' do
+ expect(subject.trigger(params)).to be_nil
+ end
+ end
+
+ context 'with a token passed' do
+ let(:project) { create(:empty_project) }
+ let(:params) { { token: 'token' } }
+
+ before do
+ allow(subject).to receive(:token).and_return('token')
+ end
+
+ context 'no user can be found' do
+ context 'when no url can be generated' do
+ it 'responds with the authorize url' do
+ response = subject.trigger(params)
+
+ expect(response[:response_type]).to eq :ephemeral
+ expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you"
+ end
+ end
+
+ context 'when an auth url can be generated' do
+ let(:params) do
+ {
+ team_domain: 'http://domain.tld',
+ team_id: 'T3423423',
+ user_id: 'U234234',
+ user_name: 'mepmep',
+ token: 'token'
+ }
+ end
+
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'generates the url' do
+ response = service.trigger(params)
+
+ expect(response[:text]).to start_with(':wave: Hi there!')
+ end
+ end
+ end
+
+ context 'when the user is authenticated' do
+ let!(:chat_name) { create(:chat_name, service: service) }
+ let(:service) do
+ project.create_mattermost_slash_commands_service(
+ properties: { token: 'token' }
+ )
+ end
+ let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ it 'triggers the command' do
+ expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute)
+
+ service.trigger(params)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipeline_email_service_spec.rb
index 1368a2925e8..4f56bceda44 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipeline_email_service_spec.rb
@@ -13,7 +13,7 @@ describe PipelinesEmailService do
end
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe 'Validations' do
@@ -23,14 +23,6 @@ describe PipelinesEmailService do
end
it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before do
- subject.add_pusher = true
- end
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
end
context 'when service is inactive' do
@@ -66,8 +58,7 @@ describe PipelinesEmailService do
end
it 'sends email' do
- sent_to = ActionMailer::Base.deliveries.flat_map(&:to)
- expect(sent_to).to contain_exactly(recipient)
+ should_only_email(double(notification_email: recipient), kind: :bcc)
end
end
@@ -79,7 +70,7 @@ describe PipelinesEmailService do
end
it 'does not send email' do
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 38cfe4ad3e3..97f818125d3 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -37,8 +37,8 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*")
expected_attachments = [
{
@@ -63,8 +63,8 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|merge request !30> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
{
@@ -90,8 +90,8 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq(
- "test.user commented on " \
- "<url|issue #20> in <somewhere.com|project_name>: " \
+ "test.user <url|commented on " \
+ "issue #20> in <somewhere.com|project_name>: " \
"*issue title*")
expected_attachments = [
{
@@ -115,8 +115,8 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("test.user commented on " \
- "<url|snippet #5> in <somewhere.com|project_name>: " \
+ expect(message.pretext).to eq("test.user <url|commented on " \
+ "snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*")
expected_attachments = [
{
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
index babb3909f56..363138a9454 100644
--- a/spec/models/project_services/slack_service/pipeline_message_spec.rb
+++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb
@@ -15,7 +15,7 @@ describe SlackService::PipelineMessage do
},
project: { path_with_namespace: 'project_name',
web_url: 'example.gitlab.com' },
- commit: { author_name: 'hacker' }
+ user: { name: 'hacker' }
}
end
@@ -48,7 +48,7 @@ describe SlackService::PipelineMessage do
def build_message(status_text = status)
"<example.gitlab.com|project_name>:" \
- " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
+ " Pipeline <example.gitlab.com/pipelines/123|#123>" \
" of <example.gitlab.com/commits/develop|develop> branch" \
" by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 0245897938c..25458e20618 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -20,6 +20,7 @@ describe Project, models: true do
it { is_expected.to have_many(:deploy_keys) }
it { is_expected.to have_many(:hooks).dependent(:destroy) }
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
+ it { is_expected.to have_many(:chat_services) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
@@ -35,6 +36,7 @@ describe Project, models: true do
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
+ it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
@@ -76,7 +78,7 @@ describe Project, models: true do
end
describe '#members & #requesters' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -295,7 +297,7 @@ describe Project, models: true do
end
end
- xdescribe "#new_issue_address" do
+ describe "#new_issue_address" do
let(:project) { create(:empty_project, path: "somewhere") }
let(:user) { create(:user) }
@@ -305,8 +307,7 @@ describe Project, models: true do
end
it 'returns the address to create a new issue' do
- token = user.authentication_token
- address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab"
+ address = "p+#{project.path_with_namespace}+#{user.incoming_email_token}@gl.ab"
expect(project.new_issue_address(user)).to eq(address)
end
@@ -497,9 +498,6 @@ describe Project, models: true do
end
it 'returns nil and does not query services when there is no external issue tracker' do
- project.build_missing_services
- project.reload
-
expect(project).not_to receive(:services)
expect(project.external_issue_tracker).to eq(nil)
@@ -507,9 +505,6 @@ describe Project, models: true do
it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
ext_project.reload # Factory returns a project with changed attributes
- ext_project.build_missing_services
- ext_project.reload
-
expect(ext_project).to receive(:services).once.and_call_original
2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
@@ -707,7 +702,7 @@ describe Project, models: true do
"/uploads/project/avatar/#{project.id}/uploads/avatar.png"
end
- it { should eq "http://localhost#{avatar_path}" }
+ it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
context 'When avatar file in git' do
@@ -719,7 +714,7 @@ describe Project, models: true do
"/#{project.namespace.name}/#{project.path}/avatar"
end
- it { should eq "http://localhost#{avatar_path}" }
+ it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
context 'when git repo is empty' do
@@ -1521,7 +1516,7 @@ describe Project, models: true do
members_project.team << [developer, :developer]
members_project.team << [master, :master]
- create(:project_group_link, project: shared_project, group: group)
+ create(:project_group_link, project: shared_project, group: group, group_access: Gitlab::Access::DEVELOPER)
end
it 'returns false for no user' do
@@ -1550,7 +1545,9 @@ describe Project, models: true do
expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false)
- expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
+ expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(false)
+ expect(shared_project.authorized_for_user?(developer, Gitlab::Access::DEVELOPER)).to be(true)
+ expect(shared_project.authorized_for_user?(master, Gitlab::Access::DEVELOPER)).to be(true)
end
end
@@ -1647,15 +1644,18 @@ describe Project, models: true do
end
it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+ expect(project.environments_for('master', commit: project.commit, with_tags: true))
+ .to contain_exactly(environment)
end
it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', project.commit)).to be_empty
+ expect(project.environments_for('master', commit: project.commit))
+ .to be_empty
end
it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ expect(project.environments_for('master', commit: project.commit('feature')))
+ .to be_empty
end
end
@@ -1665,15 +1665,65 @@ describe Project, models: true do
end
it 'returns environment when ref is set' do
- expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+ expect(project.environments_for('master', commit: project.commit))
+ .to contain_exactly(environment)
end
it 'does not environment when ref is different' do
- expect(project.environments_for('feature', project.commit)).to be_empty
+ expect(project.environments_for('feature', commit: project.commit))
+ .to be_empty
end
it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ expect(project.environments_for('master', commit: project.commit('feature')))
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(project.environments_for('master'))
+ .to contain_exactly(environment)
+ end
+ end
+ end
+
+ describe '#environments_recently_updated_on_branch' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'when last deployment to environment is the most recent one' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ end
+
+ it 'finds recently updated environment' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'when last deployment to environment is not the most recent' do
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: environment, ref: 'master')
+ end
+
+ it 'does not find environment' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to be_empty
+ end
+ end
+
+ context 'when there are two environments that deploy to the same branch' do
+ let(:second_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: environment, ref: 'feature')
+ create(:deployment, environment: second_environment, ref: 'feature')
+ end
+
+ it 'finds both environments' do
+ expect(project.environments_recently_updated_on_branch('feature'))
+ .to contain_exactly(environment, second_environment)
end
end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index e0f2dadf189..12228425579 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
let(:requester) { create(:user) }
before do
@@ -155,7 +155,7 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:project) { create(:empty_project, group: group) }
let(:requester) { create(:user) }
@@ -200,7 +200,7 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
context 'when project is not shared with group' do
before do
@@ -243,7 +243,7 @@ describe ProjectTeam, models: true do
end
context 'group project' do
- let(:group) { create(:group) }
+ let(:group) { create(:group, :access_requestable) }
let(:project) { create(:empty_project, group: group) }
before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 04b7d19d414..72ac41f3472 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -113,6 +113,26 @@ describe Repository, models: true do
end
end
+ describe '#ref_exists?' do
+ context 'when ref exists' do
+ it 'returns true' do
+ expect(repository.ref_exists?('refs/heads/master')).to be true
+ end
+ end
+
+ context 'when ref does not exist' do
+ it 'returns false' do
+ expect(repository.ref_exists?('refs/heads/non-existent')).to be false
+ end
+ end
+
+ context 'when ref format is incorrect' do
+ it 'returns false' do
+ expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
+ end
+ end
+ end
+
describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
@@ -197,6 +217,35 @@ describe Repository, models: true do
end
end
+ describe '#commit' do
+ context 'when ref exists' do
+ it 'returns commit object' do
+ expect(repository.commit('master'))
+ .to be_an_instance_of Commit
+ end
+ end
+
+ context 'when ref does not exist' do
+ it 'returns nil' do
+ expect(repository.commit('non-existent-ref')).to be_nil
+ end
+ end
+
+ context 'when ref is not valid' do
+ context 'when preceding tree element exists' do
+ it 'returns nil' do
+ expect(repository.commit('master:ref')).to be_nil
+ end
+ end
+
+ context 'when preceding tree element does not exist' do
+ it 'returns nil' do
+ expect(repository.commit('non-existent:ref')).to be_nil
+ end
+ end
+ end
+ end
+
describe "#commit_dir" do
it "commits a change that creates a new directory" do
expect do
@@ -344,24 +393,37 @@ describe Repository, models: true do
end
end
- describe "search_files" do
- let(:results) { repository.search_files('feature', 'master') }
+ describe "search_files_by_content" do
+ let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
it { is_expected.to be_an Array }
it 'regex-escapes the query string' do
- results = repository.search_files("test\\", 'master')
+ results = repository.search_files_by_content("test\\", 'master')
expect(results.first).not_to start_with('fatal:')
end
it 'properly handles an unmatched parenthesis' do
- results = repository.search_files("test(", 'master')
+ results = repository.search_files_by_content("test(", 'master')
expect(results.first).not_to start_with('fatal:')
end
+ it 'properly handles when query is not present' do
+ results = repository.search_files_by_content('', 'master')
+
+ expect(results).to match_array([])
+ end
+
+ it 'properly handles query when repo is empty' do
+ repository = create(:empty_project).repository
+ results = repository.search_files_by_content('test', 'master')
+
+ expect(results).to match_array([])
+ end
+
describe 'result' do
subject { results.first }
@@ -370,6 +432,28 @@ describe Repository, models: true do
end
end
+ describe "search_files_by_name" do
+ let(:results) { repository.search_files_by_name('files', 'master') }
+
+ it 'returns result' do
+ expect(results.first).to eq('files/html/500.html')
+ end
+
+ it 'properly handles when query is not present' do
+ results = repository.search_files_by_name('', 'master')
+
+ expect(results).to match_array([])
+ end
+
+ it 'properly handles query when repo is empty' do
+ repository = create(:empty_project).repository
+
+ results = repository.search_files_by_name('test', 'master')
+
+ expect(results).to match_array([])
+ end
+ end
+
describe '#create_ref' do
it 'redirects the call to fetch_ref' do
ref, ref_path = '1', '2'
@@ -1270,6 +1354,28 @@ describe Repository, models: true do
repository.add_tag(user, '8.5', 'master', 'foo')
end
+ it 'does not create a tag when a pre-hook fails' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
+
+ expect do
+ repository.add_tag(user, '8.5', 'master', 'foo')
+ end.to raise_error(GitHooksService::PreReceiveError)
+
+ repository.expire_tags_cache
+ expect(repository.find_tag('8.5')).to be_nil
+ end
+
+ it 'passes tag SHA to hooks' do
+ spy = GitHooksService.new
+ allow(GitHooksService).to receive(:new).and_return(spy)
+ allow(spy).to receive(:execute).and_call_original
+
+ tag = repository.add_tag(user, '8.5', 'master', 'foo')
+
+ expect(spy).to have_received(:execute).
+ with(anything, anything, anything, tag.target, anything)
+ end
+
it 'returns a Gitlab::Git::Tag object' do
tag = repository.add_tag(user, '8.5', 'master', 'foo')
@@ -1472,14 +1578,4 @@ describe Repository, models: true do
end.to raise_error(Repository::CommitError)
end
end
-
- describe '#remove_storage_from_path' do
- let(:storage_path) { project.repository_storage_path }
- let(:project_path) { project.path_with_namespace }
- let(:full_path) { File.join(storage_path, project_path) }
-
- it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
- it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
- end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 43937a54b2c..b1615a95004 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -53,7 +53,7 @@ describe Service, models: true do
describe "Template" do
describe "for pushover service" do
- let(:service_template) do
+ let!(:service_template) do
PushoverService.create(
template: true,
properties: {
@@ -66,13 +66,9 @@ describe Service, models: true do
let(:project) { create(:project) }
describe 'is prefilled for projects pushover service' do
- before do
- service_template
- project.build_missing_services
- end
-
it "has all fields prefilled" do
- service = project.pushover_service
+ service = project.find_or_initialize_service('pushover')
+
expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice')
expect(service.sound).to eq('mic')
diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb
new file mode 100644
index 00000000000..9ab112bb2ee
--- /dev/null
+++ b/spec/models/subscription_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Subscription, models: true do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:subscribable) }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:subscribable) }
+ it { is_expected.to validate_presence_of(:user) }
+
+ it 'validates uniqueness of project_id scoped to subscribable_id, subscribable_type, and user_id' do
+ create(:subscription)
+
+ expect(subject).to validate_uniqueness_of(:project_id).scoped_to([:subscribable_id, :subscribable_type, :user_id])
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ba47479a2e1..e84042f8063 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -33,11 +33,12 @@ describe User, models: true do
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
+ it { is_expected.to have_many(:chat_names).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
user = create(:user)
- group = create(:group, :public)
+ group = create(:group, :public, :access_requestable)
group.request_access(user)
expect(user.group_members).to be_empty
@@ -47,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)
+ project = create(:project, :public, :access_requestable)
project.request_access(user)
expect(user.project_members).to be_empty
@@ -490,6 +491,28 @@ describe User, models: true do
end
end
+ describe '.without_projects' do
+ let!(:project) { create(:empty_project, :public, :access_requestable) }
+ let!(:user) { create(:user) }
+ let!(:user_without_project) { create(:user) }
+ let!(:user_without_project2) { create(:user) }
+
+ before do
+ # add user to project
+ project.team << [user, :master]
+
+ # create invite to projet
+ create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com')
+
+ # create request to join project
+ project.request_access(user_without_project2)
+ end
+
+ it { expect(User.without_projects).not_to include user }
+ it { expect(User.without_projects).to include user_without_project }
+ it { expect(User.without_projects).to include user_without_project2 }
+ end
+
describe '.not_in_project' do
before do
User.delete_all
@@ -1050,7 +1073,7 @@ describe User, models: true do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects' do
+ describe '#authorized_projects', truncate: true do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -1070,6 +1093,80 @@ describe User, models: true do
.to contain_exactly(project)
end
end
+
+ it "includes user's personal projects" do
+ user = create(:user)
+ project = create(:project, :private, namespace: user.namespace)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ 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.team << [user2, Gitlab::Access::DEVELOPER]
+
+ expect(user2.authorized_projects).to include(project)
+ end
+
+ it "includes projects of groups user has been added to" do
+ group = create(:group)
+ project = create(:project, group: group)
+ user = create(:user)
+
+ group.add_developer(user)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ it "does not include projects of groups user has been removed from" do
+ group = create(:group)
+ project = create(:project, group: group)
+ user = create(:user)
+
+ member = group.add_developer(user)
+ expect(user.authorized_projects).to include(project)
+
+ member.destroy
+ expect(user.authorized_projects).not_to include(project)
+ end
+
+ it "includes projects shared with user's group" do
+ user = create(:user)
+ project = create(:project, :private)
+ group = create(:group)
+
+ group.add_reporter(user)
+ project.project_group_links.create(group: group)
+
+ expect(user.authorized_projects).to include(project)
+ end
+
+ 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.team << [user2, Gitlab::Access::DEVELOPER]
+ expect(user2.authorized_projects).to include(project)
+
+ project.destroy
+ expect(user2.authorized_projects).not_to include(project)
+ end
+
+ it "does not include projects of destroyed groups user had access to" do
+ group = create(:group)
+ project = create(:project, namespace: group)
+ user = create(:user)
+
+ group.add_developer(user)
+ expect(user.authorized_projects).to include(project)
+
+ group.destroy
+ expect(user.authorized_projects).not_to include(project)
+ end
end
describe '#projects_where_can_admin_issues' do
@@ -1205,4 +1302,40 @@ describe User, models: true do
expect(user.viewable_starred_projects).not_to include(private_project)
end
end
+
+ describe '#projects_with_reporter_access_limited_to' do
+ let(:project1) { create(:project) }
+ let(:project2) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project1.team << [user, :reporter]
+ project2.team << [user, :guest]
+ end
+
+ it 'returns the projects when using a single project ID' do
+ projects = user.projects_with_reporter_access_limited_to(project1.id)
+
+ expect(projects).to eq([project1])
+ end
+
+ it 'returns the projects when using an Array of project IDs' do
+ projects = user.projects_with_reporter_access_limited_to([project1.id])
+
+ expect(projects).to eq([project1])
+ end
+
+ it 'returns the projects when using an ActiveRecord relation' do
+ projects = user.
+ projects_with_reporter_access_limited_to(Project.select(:id))
+
+ expect(projects).to eq([project1])
+ end
+
+ it 'does not return projects you do not have reporter access to' do
+ projects = user.projects_with_reporter_access_limited_to(project2.id)
+
+ expect(projects).to be_empty
+ end
+ end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
new file mode 100644
index 00000000000..7591bfd1471
--- /dev/null
+++ b/spec/policies/issue_policy_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe IssuePolicy, models: true do
+ let(:user) { create(:user) }
+
+ describe '#rules' do
+ context 'using a regular issue' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:policies) { described_class.abilities(user, issue).to_set }
+
+ context 'with a regular user' do
+ it 'includes the read_issue permission' do
+ expect(policies).to include(:read_issue)
+ end
+
+ it 'does not include the admin_issue permission' do
+ expect(policies).not_to include(:admin_issue)
+ end
+
+ it 'does not include the update_issue permission' do
+ expect(policies).not_to include(:update_issue)
+ end
+ end
+
+ context 'with a user that is a project reporter' do
+ before do
+ project.team << [user, :reporter]
+ end
+
+ it 'includes the read_issue permission' do
+ expect(policies).to include(:read_issue)
+ end
+
+ it 'includes the admin_issue permission' do
+ expect(policies).to include(:admin_issue)
+ end
+
+ it 'includes the update_issue permission' do
+ expect(policies).to include(:update_issue)
+ end
+ end
+
+ context 'with a user that is a project guest' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'includes the read_issue permission' do
+ expect(policies).to include(:read_issue)
+ end
+
+ it 'does not include the admin_issue permission' do
+ expect(policies).not_to include(:admin_issue)
+ end
+
+ it 'does not include the update_issue permission' do
+ expect(policies).not_to include(:update_issue)
+ end
+ end
+ end
+
+ context 'using a confidential issue' do
+ let(:issue) { create(:issue, :confidential) }
+
+ context 'with a regular user' do
+ let(:policies) { described_class.abilities(user, issue).to_set }
+
+ it 'does not include the read_issue permission' do
+ expect(policies).not_to include(:read_issue)
+ end
+
+ it 'does not include the admin_issue permission' do
+ expect(policies).not_to include(:admin_issue)
+ end
+
+ it 'does not include the update_issue permission' do
+ expect(policies).not_to include(:update_issue)
+ end
+ end
+
+ context 'with a user that is a project member' do
+ let(:policies) { described_class.abilities(user, issue).to_set }
+
+ before do
+ issue.project.team << [user, :reporter]
+ end
+
+ it 'includes the read_issue permission' do
+ expect(policies).to include(:read_issue)
+ end
+
+ it 'includes the admin_issue permission' do
+ expect(policies).to include(:admin_issue)
+ end
+
+ it 'includes the update_issue permission' do
+ expect(policies).to include(:update_issue)
+ end
+ end
+
+ context 'without a user' do
+ let(:policies) { described_class.abilities(nil, issue).to_set }
+
+ it 'does not include the read_issue permission' do
+ expect(policies).not_to include(:read_issue)
+ end
+
+ it 'does not include the admin_issue permission' do
+ expect(policies).not_to include(:admin_issue)
+ end
+
+ it 'does not include the update_issue permission' do
+ expect(policies).not_to include(:update_issue)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index b467890a403..1a771b3c87a 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -9,19 +9,19 @@ describe API::AccessRequests, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(: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)
+ end
end
let(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/access_requests' do |source_type|
@@ -89,7 +89,7 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update(request_access_enabled: false)
+ source.update_attributes(request_access_enabled: false)
end
it 'returns 403' do
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
new file mode 100644
index 00000000000..be4bc39ada2
--- /dev/null
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe ::API::Helpers::InternalHelpers do
+ include ::API::Helpers::InternalHelpers
+
+ describe '.clean_project_path' do
+ project = 'namespace/project'
+ namespaced = File.join('namespace2', project)
+
+ {
+ File.join(Dir.pwd, project) => project,
+ File.join(Dir.pwd, namespaced) => namespaced,
+ project => project,
+ namespaced => namespaced,
+ project + '.git' => project,
+ namespaced + '.git' => namespaced,
+ "/" + project => project,
+ "/" + namespaced => namespaced,
+ }.each do |project_path, expected|
+ context project_path do
+ # Relative and absolute storage paths, with and without trailing /
+ ['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
+ context "storage path is #{storage_path}" do
+ subject { clean_project_path(project_path, [storage_path]) }
+
+ it { is_expected.to eq(expected) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 1711096f4bd..8f605757186 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -299,4 +299,20 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Cannot remove HEAD branch')
end
end
+
+ describe "DELETE /projects/:id/repository/merged_branches" do
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
+ end
+
+ it 'returns 200' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 403 error if guest' do
+ delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ expect(response).to have_http_status(403)
+ end
+ end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7b47bf5afc1..d9fdafde05e 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -57,13 +57,66 @@ describe API::API, api: true do
end
context "when using all_available in request" do
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
it "returns all groups you have access to" do
public_group = create :group, :public
get api("/groups", user1), all_available: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(public_group.name)
+ expect(response_groups).to contain_exactly(public_group.name, group1.name)
+ end
+ end
+
+ context "when using sorting" do
+ let(:group3) { create(:group, name: "a#{group1.name}", path: "z#{group1.path}") }
+ let(:response_groups) { json_response.map { |group| group['name'] } }
+
+ before do
+ group3.add_owner(user1)
+ end
+
+ it "sorts by name ascending by default" do
+ get api("/groups", user1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group3.name, group1.name])
+ end
+
+ it "sorts in descending order when passed" do
+ get api("/groups", user1), sort: "desc"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+
+ it "sorts by the order_by param" do
+ get api("/groups", user1), order_by: "path"
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_groups).to eq([group1.name, group3.name])
+ end
+ end
+ end
+
+ describe 'GET /groups/owned' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/groups/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as group owner' do
+ it 'returns an array of groups the user owns' do
+ get api('/groups/owned', user2)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(group2.name)
end
end
end
@@ -149,7 +202,7 @@ describe API::API, api: true do
end
it 'returns 404 for a non existing group' do
- put api('/groups/1328', user1)
+ put api('/groups/1328', user1), name: new_group_name
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f0f590b0331..e88a7e27d45 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let(:project) { create(:project) }
- let(:secret_token) { File.read Gitlab.config.gitlab_shell.secret_file }
+ let(:secret_token) { Gitlab::Shell.secret_token }
describe "GET /internal/check", no_db: true do
it do
@@ -191,6 +191,26 @@ describe API::API, api: true do
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
end
+
+ context 'project as /namespace/project' do
+ it do
+ pull(key, project_with_repo_path('/' + project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
+
+ context 'project as namespace/project' do
+ it do
+ pull(key, project_with_repo_path(project.path_with_namespace))
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ end
+ end
end
end
@@ -299,7 +319,7 @@ describe API::API, api: true do
context 'project does not exist' do
it do
- pull(key, OpenStruct.new(path_with_namespace: 'gitlab/notexists'))
+ pull(key, project_with_repo_path('gitlab/notexist'))
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
@@ -386,17 +406,23 @@ describe API::API, api: true do
it 'returns link to create new merge request' do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
end
+ def project_with_repo_path(path)
+ double().tap do |fake_project|
+ allow(fake_project).to receive_message_chain('repository.path_to_repo' => path)
+ end
+ end
+
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-pack',
secret_token: secret_token,
protocol: protocol
@@ -408,7 +434,7 @@ describe API::API, api: true do
api("/internal/allowed"),
changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-receive-pack',
secret_token: secret_token,
protocol: protocol
@@ -420,7 +446,7 @@ describe API::API, api: true do
api("/internal/allowed"),
ref: 'master',
key_id: key.id,
- project: project.path_with_namespace,
+ project: project.repository.path_to_repo,
action: 'git-upload-archive',
secret_token: secret_token,
protocol: 'ssh'
@@ -432,7 +458,7 @@ describe API::API, api: true do
api("/internal/lfs_authenticate"),
key_id: key_id,
secret_token: secret_token,
- project: project.path_with_namespace
+ project: project.repository.path_to_repo
)
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index beed53d1e5c..7bae055b241 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -637,7 +637,7 @@ describe API::API, api: true do
it "sends notifications for subscribers of newly added labels" do
label = project.labels.first
- label.toggle_subscription(user2)
+ label.toggle_subscription(user2, project)
perform_enqueued_jobs do
post api("/projects/#{project.id}/issues", user),
@@ -828,7 +828,7 @@ describe API::API, api: true do
it "sends notifications for subscribers of newly added labels when issue is updated" do
label = create(:label, title: 'foo', color: '#FFAABB', project: project)
- label.toggle_subscription(user2)
+ label.toggle_subscription(user2, project)
perform_enqueued_jobs do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 7e532912d08..aaf41639277 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -15,23 +15,56 @@ describe API::API, api: true do
describe 'GET /projects/:id/labels' do
it 'returns all available labels to the project' do
group = create(:group)
- group_label = create(:group_label, group: group)
+ group_label = create(:group_label, title: 'feature', group: group)
project.update(group: group)
+ create(:labeled_issue, project: project, labels: [group_label], author: user)
+ create(:labeled_issue, project: project, labels: [label1], author: user, state: :closed)
+ create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project )
+
+ expected_keys = [
+ 'id', 'name', 'color', 'description',
+ 'open_issues_count', 'closed_issues_count', 'open_merge_requests_count',
+ 'subscribed', 'priority'
+ ]
get api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq(3)
+ expect(json_response.first.keys).to match_array expected_keys
expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, priority_label.name, label1.name])
- expect(json_response.last['name']).to eq(label1.name)
- expect(json_response.last['color']).to be_present
- expect(json_response.last['description']).to be_nil
- expect(json_response.last['open_issues_count']).to eq(0)
- expect(json_response.last['closed_issues_count']).to eq(0)
- expect(json_response.last['open_merge_requests_count']).to eq(0)
- expect(json_response.last['priority']).to be_nil
- expect(json_response.last['subscribed']).to be_falsey
+
+ label1_response = json_response.find { |l| l['name'] == label1.title }
+ group_label_response = json_response.find { |l| l['name'] == group_label.title }
+ priority_label_response = json_response.find { |l| l['name'] == priority_label.title }
+
+ expect(label1_response['open_issues_count']).to eq(0)
+ expect(label1_response['closed_issues_count']).to eq(1)
+ expect(label1_response['open_merge_requests_count']).to eq(0)
+ expect(label1_response['name']).to eq(label1.name)
+ expect(label1_response['color']).to be_present
+ expect(label1_response['description']).to be_nil
+ expect(label1_response['priority']).to be_nil
+ expect(label1_response['subscribed']).to be_falsey
+
+ expect(group_label_response['open_issues_count']).to eq(1)
+ expect(group_label_response['closed_issues_count']).to eq(0)
+ expect(group_label_response['open_merge_requests_count']).to eq(0)
+ expect(group_label_response['name']).to eq(group_label.name)
+ expect(group_label_response['color']).to be_present
+ expect(group_label_response['description']).to be_nil
+ expect(group_label_response['priority']).to be_nil
+ expect(group_label_response['subscribed']).to be_falsey
+
+ expect(priority_label_response['open_issues_count']).to eq(0)
+ expect(priority_label_response['closed_issues_count']).to eq(0)
+ expect(priority_label_response['open_merge_requests_count']).to eq(1)
+ expect(priority_label_response['name']).to eq(priority_label.name)
+ expect(priority_label_response['color']).to be_present
+ expect(priority_label_response['description']).to be_nil
+ expect(priority_label_response['priority']).to eq(3)
+ expect(priority_label_response['subscribed']).to be_falsey
end
end
@@ -306,7 +339,7 @@ describe API::API, api: true do
end
context "when user is already subscribed to label" do
- before { label1.subscribe(user) }
+ before { label1.subscribe(user, project) }
it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
@@ -325,7 +358,7 @@ describe API::API, api: true do
end
describe "DELETE /projects/:id/labels/:label_id/subscription" do
- before { label1.subscribe(user) }
+ before { label1.subscribe(user, project) }
context "when label_id is a label title" do
it "unsubscribes from the label" do
@@ -348,7 +381,7 @@ describe API::API, api: true do
end
context "when user is already unsubscribed from label" do
- before { label1.unsubscribe(user) }
+ before { label1.unsubscribe(user, project) }
it "returns 304" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 493c0a893d1..2c94c86ccfa 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -9,19 +9,19 @@ describe API::Members, api: true do
let(:stranger) { create(:user) }
let(:project) do
- project = create(:project, :public, creator_id: master.id, namespace: master.namespace)
- project.team << [developer, :developer]
- project.team << [master, :master]
- project.request_access(access_requester)
- project
+ create(: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)
+ end
end
let!(:group) do
- group = create(:group, :public)
- group.add_developer(developer)
- group.add_owner(master)
- group.request_access(access_requester)
- group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ end
end
shared_examples 'GET /:sources/:id/members' do |source_type|
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index bae4fa11ec2..7b3d1460c90 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -494,12 +494,6 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "returns 400 when source_branch is specified" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
- source_branch: "master", target_branch: "master"
- expect(response).to have_http_status(400)
- end
-
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index dd192bea432..62327f64e50 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -123,6 +123,15 @@ describe API::API, api: true do
expect(json_response['title']).to eq('updated title')
end
+ it 'removes a due date if nil is passed' do
+ milestone.update!(due_date: "2016-08-05")
+
+ put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to be_nil
+ end
+
it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 7011bdc9ec0..d83f7883c78 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -41,6 +41,52 @@ describe API::API, api: true do
end
end
+ describe 'POST /projects/:id/pipeline ' do
+ context 'authorized user' do
+ context 'with gitlab-ci.yml' do
+ before { stub_ci_pipeline_to_return_yaml_file }
+
+ it 'creates and returns a new pipeline' do
+ expect do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+ end.to change { Ci::Pipeline.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to be_a Hash
+ expect(json_response['sha']).to eq project.commit.id
+ end
+
+ it 'fails when using an invalid ref' do
+ post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Reference not found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+
+ context 'without gitlab-ci.yml' do
+ it 'fails to create pipeline' do
+ post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not create pipeline' do
+ post api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response).not_to be_an Array
+ end
+ end
+ end
+
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
it 'returns project pipelines' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3c8f0ac531a..f020d471422 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -175,6 +175,30 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/owned' do
+ before do
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/projects/owned')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'returns an array of projects the user owns' do
+ get api('/projects/owned', user4)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project4.name)
+ expect(json_response.first['owner']['username']).to eq(user4.username)
+ end
+ end
+ end
+
describe 'GET /projects/visible' do
let(:public_project) { create(:project, :public) }
@@ -336,6 +360,14 @@ describe API::API, api: true do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
+ it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
+
+ post api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
+ end
+
it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index c4dc2d9006a..38c8ad34f9d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -18,6 +18,7 @@ describe API::API, api: true do
it "returns project commits" do
get api("/projects/#{project.id}/repository/tree", user)
+
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -43,6 +44,40 @@ describe API::API, api: true do
end
end
+
+ describe 'GET /projects/:id/repository/tree?recursive=1' do
+ context 'authorized user' do
+ before { project.team << [user2, :reporter] }
+
+ it 'should return recursive project paths tree' do
+ get api("/projects/#{project.id}/repository/tree?recursive=1", user)
+
+ expect(response.status).to eq(200)
+
+ expect(json_response).to be_an Array
+ expect(json_response[4]['name']).to eq('html')
+ expect(json_response[4]['path']).to eq('files/html')
+ expect(json_response[4]['type']).to eq('tree')
+ expect(json_response[4]['mode']).to eq('040000')
+ end
+
+ it 'returns a 404 for unknown ref' do
+ get api("/projects/#{project.id}/repository/tree?ref_name=foo&recursive=1", user)
+ expect(response).to have_http_status(404)
+
+ expect(json_response).to be_an Object
+ json_response['message'] == '404 Tree Not Found'
+ end
+ end
+
+ context "unauthorized user" do
+ it "does not return project commits" do
+ get api("/projects/#{project.id}/repository/tree?recursive=1")
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe "GET /projects/:id/repository/blobs/:sha" do
it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index f46f016135e..99414270be6 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -226,7 +226,7 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
it 'does not update runner' do
- put api("/runners/#{shared_runner.id}", user)
+ put api("/runners/#{shared_runner.id}", user), description: 'test'
expect(response).to have_http_status(403)
end
@@ -234,7 +234,7 @@ describe API::Runners, api: true do
context 'when runner is not shared' do
it 'does not update runner without access to it' do
- put api("/runners/#{specific_runner.id}", user2)
+ put api("/runners/#{specific_runner.id}", user2), description: 'test'
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 375671bca4c..ce9c96ace21 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -56,8 +56,7 @@ describe API::API, api: true do
# inject some properties into the service
before do
- project.build_missing_services
- service_object = project.send(service_method)
+ service_object = project.find_or_initialize_service(service)
service_object.properties = service_attrs
service_object.save
end
@@ -89,4 +88,61 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/services/:slug/trigger' do
+ let!(:project) { create(:empty_project) }
+ let(:service_name) { 'mattermost_slash_commands' }
+
+ context 'no service is available' do
+ it 'returns a not found message' do
+ post api("/projects/#{project.id}/services/idonotexist/trigger")
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to eq("404 Service Not Found")
+ end
+ end
+
+ context 'the service exists' do
+ let(:params) { { token: 'token' } }
+
+ context 'the service is not active' do
+ let!(:inactive_service) do
+ project.create_mattermost_slash_commands_service(
+ active: false,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'when the service is inactive' do
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger")
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'the service is active' do
+ let!(:active_service) do
+ project.create_mattermost_slash_commands_service(
+ active: true,
+ properties: { token: 'token' }
+ )
+ end
+
+ it 'retusn status 200' do
+ post api("/projects/#{project.id}/services/mattermost_slash_commands/trigger"), params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when the project can not be found' do
+ it 'returns a generic 404' do
+ post api("/projects/404/services/mattermost_slash_commands/trigger"), params
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to eq("404 Service Not Found")
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index acad1365ace..e3f22b4c578 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -67,22 +67,24 @@ describe API::API, api: true do
end
context "when empty password" do
- it "returns authentication error" do
+ it "returns authentication error with email" do
post api("/session"), email: user.email
- expect(response).to have_http_status(401)
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns authentication error with username" do
+ post api("/session"), email: user.username
+
+ expect(response).to have_http_status(400)
end
end
context "when empty name" do
it "returns authentication error" do
post api("/session"), password: user.password
- expect(response).to have_http_status(401)
- expect(json_response['email']).to be_nil
- expect(json_response['private_token']).to be_nil
+ expect(response).to have_http_status(400)
end
end
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index f685a3685e6..6c9df21f598 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -52,6 +52,12 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
end
+ it "responds with 400 if url is invalid" do
+ post api("/hooks", admin), url: 'hp://mep.mep'
+
+ expect(response).to have_http_status(400)
+ end
+
it "does not create new hook without url" do
expect do
post api("/hooks", admin)
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 82bba1ce8a4..c890a51ae42 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -54,6 +54,13 @@ describe API::API do
expect(pipeline.builds.size).to eq(5)
end
+ it 'creates builds on webhook from other gitlab repository and branch' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
@@ -68,7 +75,7 @@ describe API::API do
it 'validates variables to be a hash' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('variables needs to be a hash')
+ expect(json_response['error']).to eq('variables is invalid')
end
it 'validates variables needs to be a map of key-valued strings' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ae8639d78d5..1a6e7716b2f 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -48,6 +48,17 @@ describe API::API, api: true do
end['username']).to eq(username)
end
+ it "returns an array of blocked users" do
+ ldap_blocked_user
+ create(:user, state: 'blocked')
+
+ get api("/users?blocked=true", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
+ end
+
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response).to have_http_status(200)
@@ -69,6 +80,16 @@ describe API::API, api: true do
expect(json_response.first.keys).to include 'last_sign_in_at'
expect(json_response.first.keys).to include 'confirmed_at'
end
+
+ it "returns an array of external users" do
+ create(:user, external: true)
+
+ get api("/users?external=true", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to all(include('external' => true))
+ end
end
end
@@ -87,7 +108,7 @@ describe API::API, api: true do
it "returns a 404 error if user id not found" do
get api("/users/9999", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
@@ -338,7 +359,7 @@ describe API::API, api: true do
it "returns 404 for non-existing user" do
put api("/users/999999", admin), { bio: 'update should fail' }
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 if invalid ID" do
@@ -366,6 +387,18 @@ describe API::API, api: true do
to eq([Gitlab::Regex.namespace_regex_message])
end
+ it 'returns 400 if provider is missing for identity update' do
+ put api("/users/#{omniauth_user.id}", admin), extern_uid: '654321'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 if external UID is missing for identity update' do
+ put api("/users/#{omniauth_user.id}", admin), provider: 'ldap'
+
+ expect(response).to have_http_status(400)
+ end
+
context "with existing user" do
before do
post api("/users", admin), { email: 'test@example.com', password: 'password', username: 'test', name: 'test' }
@@ -393,14 +426,16 @@ describe API::API, api: true do
it "does not create invalid ssh key" do
post api("/users/#{user.id}/keys", admin), { title: "invalid key" }
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "key" not given')
+ expect(json_response['error']).to eq('key is missing')
end
it 'does not create key without title' do
post api("/users/#{user.id}/keys", admin), key: 'some key'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "title" not given')
+ expect(json_response['error']).to eq('title is missing')
end
it "creates ssh key" do
@@ -416,7 +451,7 @@ describe API::API, api: true do
end
end
- describe 'GET /user/:uid/keys' do
+ describe 'GET /user/:id/keys' do
before { admin }
context 'when unauthenticated' do
@@ -444,7 +479,7 @@ describe API::API, api: true do
end
end
- describe 'DELETE /user/:uid/keys/:id' do
+ describe 'DELETE /user/:id/keys/:key_id' do
before { admin }
context 'when unauthenticated' do
@@ -485,8 +520,9 @@ describe API::API, api: true do
it "does not create invalid email" do
post api("/users/#{user.id}/emails", admin), {}
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "email" not given')
+ expect(json_response['error']).to eq('email is missing')
end
it "creates email" do
@@ -503,7 +539,7 @@ describe API::API, api: true do
end
end
- describe 'GET /user/:uid/emails' do
+ describe 'GET /user/:id/emails' do
before { admin }
context 'when unauthenticated' do
@@ -537,7 +573,7 @@ describe API::API, api: true do
end
end
- describe 'DELETE /user/:uid/emails/:id' do
+ describe 'DELETE /user/:id/emails/:email_id' do
before { admin }
context 'when unauthenticated' do
@@ -652,7 +688,7 @@ describe API::API, api: true do
end
end
- describe "GET /user/keys/:id" do
+ describe "GET /user/keys/:key_id" do
it "returns single key" do
user.keys << key
user.save
@@ -665,7 +701,7 @@ describe API::API, api: true do
get api("/user/keys/42", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 error if admin accesses user's ssh key" do
@@ -674,7 +710,7 @@ describe API::API, api: true do
admin
get api("/user/keys/#{key.id}", admin)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 404 for invalid ID" do
@@ -700,14 +736,16 @@ describe API::API, api: true do
it "does not create ssh key without key" do
post api("/user/keys", user), title: 'title'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "key" not given')
+ expect(json_response['error']).to eq('key is missing')
end
it 'does not create ssh key without title' do
post api('/user/keys', user), key: 'some key'
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "title" not given')
+ expect(json_response['error']).to eq('title is missing')
end
it "does not create ssh key without title" do
@@ -716,7 +754,7 @@ describe API::API, api: true do
end
end
- describe "DELETE /user/keys/:id" do
+ describe "DELETE /user/keys/:key_id" do
it "deletes existed key" do
user.keys << key
user.save
@@ -726,9 +764,11 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "returns success if key ID not found" do
+ it "returns 404 if key ID not found" do
delete api("/user/keys/42", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Key Not Found')
end
it "returns 401 error if unauthorized" do
@@ -765,7 +805,7 @@ describe API::API, api: true do
end
end
- describe "GET /user/emails/:id" do
+ describe "GET /user/emails/:email_id" do
it "returns single email" do
user.emails << email
user.save
@@ -777,7 +817,7 @@ describe API::API, api: true do
it "returns 404 Not Found within invalid ID" do
get api("/user/emails/42", user)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 error if admin accesses user's email" do
@@ -786,7 +826,7 @@ describe API::API, api: true do
admin
get api("/user/emails/#{email.id}", admin)
expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 404 for invalid ID" do
@@ -812,12 +852,13 @@ describe API::API, api: true do
it "does not create email with invalid email" do
post api("/user/emails", user), {}
+
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('400 (Bad request) "email" not given')
+ expect(json_response['error']).to eq('email is missing')
end
end
- describe "DELETE /user/emails/:id" do
+ describe "DELETE /user/emails/:email_id" do
it "deletes existed email" do
user.emails << email
user.save
@@ -827,9 +868,11 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "returns success if email ID not found" do
+ it "returns 404 if email ID not found" do
delete api("/user/emails/42", user)
- expect(response).to have_http_status(200)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Email Not Found')
end
it "returns 401 error if unauthorized" do
@@ -839,10 +882,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "returns a 404 for invalid ID" do
- delete api("/users/emails/ASDF", admin)
+ it "returns 400 for invalid ID" do
+ delete api("/user/emails/ASDF", admin)
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(400)
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 6d49c42c215..a611c5e3823 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -213,26 +213,102 @@ describe Ci::API::API do
let(:build) { create(:ci_build, :pending, :trace, runner_id: runner.id) }
let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+ let(:update_interval) { 10.seconds.to_i }
+
+ def patch_the_trace(content = ' appended', request_headers = nil)
+ unless request_headers
+ offset = build.trace_length
+ limit = offset + content.length - 1
+ request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
+ end
+
+ Timecop.travel(build.updated_at + update_interval) do
+ patch ci_api("/builds/#{build.id}/trace.txt"), content, request_headers
+ build.reload
+ end
+ end
+
+ def initial_patch_the_trace
+ patch_the_trace(' appended', headers_with_range)
+ end
+
+ def force_patch_the_trace
+ 2.times { patch_the_trace('') }
+ end
before do
build.run!
- patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
+ initial_patch_the_trace
end
context 'when request is valid' do
it 'gets correct response' do
expect(response.status).to eq 202
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
expect(response.header).to have_key 'Range'
expect(response.header).to have_key 'Build-Status'
end
- it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
+ context 'when build has been updated recently' do
+ it { expect{ patch_the_trace }.not_to change { build.updated_at }}
+
+ it 'changes the build trace' do
+ patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect{ force_patch_the_trace }.not_to change { build.updated_at }}
+
+ it "doesn't change the build.trace" do
+ force_patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+
+ context 'when build was not updated recently' do
+ let(:update_interval) { 15.minutes.to_i }
+
+ it { expect { patch_the_trace }.to change { build.updated_at } }
+
+ it 'changes the build.trace' do
+ patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended appended'
+ end
+
+ context 'when Runner makes a force-patch' do
+ it { expect { force_patch_the_trace }.to change { build.updated_at } }
+
+ it "doesn't change the build.trace" do
+ force_patch_the_trace
+
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ end
+ end
+ end
+ end
+
+ context 'when Runner makes a force-patch' do
+ before do
+ force_patch_the_trace
+ end
+
+ it 'gets correct response' do
+ expect(response.status).to eq 202
+ expect(build.reload.trace).to eq 'BUILD TRACE appended'
+ expect(response.header).to have_key 'Range'
+ expect(response.header).to have_key 'Build-Status'
+ end
end
context 'when content-range start is too big' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
- it 'gets correct response' do
+ it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
@@ -242,7 +318,7 @@ describe Ci::API::API do
context 'when content-range start is too small' do
let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
- it 'gets correct response' do
+ it 'gets 416 error response with range headers' do
expect(response.status).to eq 416
expect(response.header).to have_key 'Range'
expect(response.header['Range']).to eq '0-11'
@@ -250,7 +326,7 @@ describe Ci::API::API do
end
context 'when Content-Range header is missing' do
- let(:headers_with_range) { headers.merge({}) }
+ let(:headers_with_range) { headers }
it { expect(response.status).to eq 400 }
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 27f0fd22ae6..f1728d61def 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -115,6 +115,38 @@ describe 'Git HTTP requests', lib: true do
end.to raise_error(JWT::DecodeError)
end
end
+
+ context 'when the repo is public' do
+ context 'but the repo is disabled' do
+ it 'does not allow to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ context 'but the repo is enabled' do
+ it 'allows to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ context 'but only project members are allowed' do
+ it 'does not allow to clone the repo' do
+ project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE)
+
+ download("#{project.path_with_namespace}.git", {}) do |response|
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+ end
end
context "when the project is private" do
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index f0ef155bd7b..a3e7844b2f3 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -20,7 +20,7 @@ describe JwtController do
end
end
- context 'when using authorized request' do
+ context 'when using authenticated request' do
context 'using CI token' do
let(:build) { create(:ci_build, :running) }
let(:project) { build.project }
@@ -65,7 +65,7 @@ describe JwtController do
let(:access_token) { create(:personal_access_token, user: user) }
let(:headers) { { authorization: credentials(user.username, access_token.token) } }
- it 'rejects the authorization attempt' do
+ it 'accepts the authorization attempt' do
expect(response).to have_http_status(200)
end
end
@@ -81,6 +81,20 @@ describe JwtController do
end
end
+ context 'when using unauthenticated request' do
+ it 'accepts the authorization attempt' do
+ get '/jwt/auth', parameters
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'allows read access' do
+ expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
+
+ get '/jwt/auth', parameters
+ end
+ end
+
context 'unknown service' do
subject! { get '/jwt/auth', service: 'unknown' }
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
new file mode 100644
index 00000000000..705dbb7d1c0
--- /dev/null
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'cycle analytics events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
+
+ describe 'GET /:namespace/:project/cycle_analytics/events/issues' do
+ before do
+ project.team << [user, :developer]
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+
+ 3.times { create_cycle }
+ deploy_master
+
+ login_as(user)
+ end
+
+ 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
+
+ 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
+
+ expect(json_response['events'].first['short_sha']).to eq(MergeRequest.last.commits.first.short_id)
+ end
+
+ it 'lists the code events' do
+ get namespace_project_cycle_analytics_code_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
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the test 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
+
+ expect(json_response['events'].first['iid']).to eq(first_mr_iid)
+ end
+
+ it 'lists the staging 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
+
+ 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
+
+ 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)
+ end
+
+ it 'does not list the test events' do
+ get namespace_project_cycle_analytics_test_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'does not list the staging events' do
+ get namespace_project_cycle_analytics_staging_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:not_found)
+ end
+
+ it 'lists the issue events' do
+ get namespace_project_cycle_analytics_issue_path(project.namespace, project, format: :json)
+
+ expect(response).to have_http_status(:ok)
+ end
+ end
+ end
+
+ def json_response
+ JSON.parse(response.body)
+ end
+
+ def create_cycle
+ milestone = create(:milestone, project: project)
+ issue.update(milestone: milestone)
+ mr = create_merge_request_closing_issue(issue)
+
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha)
+ pipeline.run
+
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+ create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(issue)
+
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.sha)
+ end
+end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index c18a2d55e43..7aba4f08088 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -261,20 +261,28 @@ describe "Authentication", "routing" do
end
describe "Groups", "routing" do
+ let(:name) { 'complex.group-name' }
+
it "to #show" do
- expect(get("/groups/1")).to route_to('groups#show', id: '1')
+ expect(get("/groups/#{name}")).to route_to('groups#show', id: name)
end
it "also display group#show on the short path" do
- allow(Group).to receive(:find_by_path).and_return(true)
+ allow(Group).to receive(:find_by).and_return(true)
+
+ expect(get("/#{name}")).to route_to('groups#show', id: name)
+ end
- expect(get('/1')).to route_to('groups#show', id: '1')
+ it "to #activity" do
+ expect(get("/groups/#{name}/activity")).to route_to('groups#activity', id: name)
end
- it "also display group#show with dot in the path" do
- allow(Group).to receive(:find_by_path).and_return(true)
+ it "to #issues" do
+ expect(get("/groups/#{name}/issues")).to route_to('groups#issues', id: name)
+ end
- expect(get('/group.with.dot')).to route_to('groups#show', id: 'group.with.dot')
+ it "to #members" do
+ expect(get("/groups/#{name}/group_members")).to route_to('groups/group_members#index', group_id: name)
end
end
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
new file mode 100644
index 00000000000..9ac6f20fd3c
--- /dev/null
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe AnalyticsBuildEntity do
+ let(:entity) do
+ described_class.new(build, request: double)
+ end
+
+ context 'build with an author' do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, author: user) }
+
+ subject { entity.as_json }
+
+ it 'contains the URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
new file mode 100644
index 00000000000..a0a9d9a5f12
--- /dev/null
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe AnalyticsBuildSerializer do
+ let(:serializer) do
+ described_class
+ .new.represent(resource)
+ end
+
+ let(:json) { serializer.as_json }
+ let(:resource) { create(:ci_build) }
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of analyticsBuild' do
+ expect(json)
+ .to include(:name, :branch, :short_sha, :date, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_generic_entity_spec.rb b/spec/serializers/analytics_generic_entity_spec.rb
new file mode 100644
index 00000000000..68086216ba9
--- /dev/null
+++ b/spec/serializers/analytics_generic_entity_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe AnalyticsIssueEntity do
+ let(:user) { create(:user) }
+ let(:entity_hash) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:request) { EntityRequest.new(project: project, entity: :merge_request) }
+
+ let(:entity) do
+ described_class.new(entity_hash, request: request, project: project)
+ end
+
+ context 'generic entity' do
+ subject { entity.as_json }
+
+ it 'contains the entity URL' do
+ expect(subject).to include(:url)
+ end
+
+ it 'contains the author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'does not contain sensitive information' do
+ expect(subject).not_to include(/token/)
+ expect(subject).not_to include(/variables/)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
new file mode 100644
index 00000000000..2842e1ba52f
--- /dev/null
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe AnalyticsIssueSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user,
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of the issue' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author)
+ end
+ end
+end
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
new file mode 100644
index 00000000000..564207984df
--- /dev/null
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe AnalyticsMergeRequestSerializer do
+ let(:serializer) do
+ described_class
+ .new(project: project, entity: :merge_request)
+ .represent(resource)
+ end
+
+ let(:user) { create(:user) }
+ let(:json) { serializer.as_json }
+ let(:project) { create(:project) }
+ let(:resource) do
+ {
+ total_time: "172802.724419",
+ title: "Eos voluptatem inventore in sed.",
+ iid: "1",
+ id: "1",
+ state: 'open',
+ created_at: "2016-11-12 15:04:02.948604",
+ author: user
+ }
+ end
+
+ context 'when there is a single object provided' do
+ it 'it generates payload for single object' do
+ expect(json).to be_an_instance_of Hash
+ end
+
+ it 'contains important elements of the merge request' do
+ expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
+ end
+ end
+end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 2734f5bedca..6dcfaec259e 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -10,9 +10,9 @@ describe BuildEntity do
context 'when build is a regular job' do
let(:build) { create(:ci_build) }
- it 'contains url to build page and retry action' do
- expect(subject).to include(:build_url, :retry_url)
- expect(subject).not_to include(:play_url)
+ it 'contains paths to build page and retry action' do
+ expect(subject).to include(:build_path, :retry_path)
+ expect(subject).not_to include(:play_path)
end
it 'does not contain sensitive information' do
@@ -24,8 +24,8 @@ describe BuildEntity do
context 'when build is a manual action' do
let(:build) { create(:ci_build, :manual) }
- it 'contains url to play action' do
- expect(subject).to include(:play_url)
+ it 'contains path to play action' do
+ expect(subject).to include(:play_path)
end
end
end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index 628e35c9a28..15f11ac3df9 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -31,7 +31,11 @@ describe CommitEntity do
end
end
- it 'contains commit URL' do
+ it 'contains path to commit' do
+ expect(subject).to include(:commit_path)
+ end
+
+ it 'contains URL to commit' do
expect(subject).to include(:commit_url)
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 51b6de91571..ea87771e2a2 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -15,6 +15,6 @@ describe DeploymentEntity do
it 'exposes nested information about branch' do
expect(subject[:ref][:name]).to eq 'master'
- expect(subject[:ref][:ref_url]).not_to be_empty
+ expect(subject[:ref][:ref_path]).not_to be_empty
end
end
diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb
new file mode 100644
index 00000000000..b9cc2f64831
--- /dev/null
+++ b/spec/serializers/entity_date_helper_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe EntityDateHelper do
+ let(:date_helper_class) { Class.new { include EntityDateHelper }.new }
+
+ it 'converts 0 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(0)).to eq(seconds: 0)
+ end
+
+ it 'converts 40 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(40)).to eq(seconds: 40)
+ end
+
+ it 'converts 60 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(60)).to eq(mins: 1)
+ end
+
+ it 'converts 70 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(70)).to eq(mins: 1, seconds: 10)
+ end
+
+ it 'converts 3600 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3600)).to eq(hours: 1)
+ end
+
+ it 'converts 3750 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(3750)).to eq(hours: 1, mins: 2, seconds: 30)
+ end
+
+ it 'converts 86400 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86400)).to eq(days: 1)
+ end
+
+ it 'converts 86560 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
+ end
+
+ it 'converts 86760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(99760)).to eq(days: 1, hours: 3, mins: 42, seconds: 40)
+ end
+
+ it 'converts 986760 seconds' do
+ expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
+ end
+end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 4ca8c299147..57728ce3181 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -13,6 +13,6 @@ describe EnvironmentEntity do
end
it 'exposes core elements of environment' do
- expect(subject).to include(:id, :name, :state, :environment_url)
+ expect(subject).to include(:id, :name, :state, :environment_path)
end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 37bc086826c..8f95c9250b0 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -33,7 +33,7 @@ describe EnvironmentSerializer do
it 'contains important elements of environment' do
expect(json)
- .to include(:name, :external_url, :environment_url, :last_deployment)
+ .to include(:name, :external_url, :environment_path, :last_deployment)
end
it 'contains relevant information about last deployment' do
diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
new file mode 100644
index 00000000000..d29e0addb53
--- /dev/null
+++ b/spec/services/after_branch_delete_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe AfterBranchDeleteService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ it 'stops environments attached to branch' do
+ expect(service).to receive(:stop_environments)
+
+ service.execute('feature')
+ end
+ end
+end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
new file mode 100644
index 00000000000..d50bfb0492c
--- /dev/null
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ChatNames::AuthorizeUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'when all parameters are valid' do
+ let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } }
+
+ it 'requests a new token' do
+ is_expected.to be_url
+ end
+ end
+
+ context 'when there are missing parameters' do
+ let(:params) { {} }
+
+ it 'does not request a new token' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
new file mode 100644
index 00000000000..51441e8f3be
--- /dev/null
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe ChatNames::FindUserService, services: true do
+ describe '#execute' do
+ let(:service) { create(:service) }
+
+ subject { described_class.new(service, params).execute }
+
+ context 'find user mapping' do
+ let(:user) { create(:user) }
+ let!(:chat_name) { create(:chat_name, user: user, service: service) }
+
+ context 'when existing user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
+
+ it 'returns the existing user' do
+ is_expected.to eq(user)
+ end
+
+ it 'updates when last time chat name was used' do
+ subject
+
+ expect(chat_name.reload.last_used_at).to be_like_time(Time.now)
+ end
+ end
+
+ context 'when different user is requested' do
+ let(:params) { { team_id: chat_name.team_id, user_id: 'non-existing-user' } }
+
+ it 'returns existing user' do
+ is_expected.to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/send_pipeline_notification_service_spec.rb b/spec/services/ci/send_pipeline_notification_service_spec.rb
deleted file mode 100644
index 288302cc94f..00000000000
--- a/spec/services/ci/send_pipeline_notification_service_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe Ci::SendPipelineNotificationService, services: true do
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit('master').sha,
- user: user,
- status: status)
- end
-
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- subject{ described_class.new(pipeline) }
-
- describe '#execute' do
- before do
- reset_delivered_emails!
- end
-
- shared_examples 'sending emails' do
- it 'sends an email to pipeline user' do
- perform_enqueued_jobs do
- subject.execute([user.email])
- end
-
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include(email_subject)
- expect(email.to).to eq([user.email])
- end
- end
-
- context 'with success pipeline' do
- let(:status) { 'success' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
-
- it_behaves_like 'sending emails'
- end
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
-
- it_behaves_like 'sending emails'
- end
- end
-end
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
new file mode 100644
index 00000000000..6f7d1a5d28d
--- /dev/null
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Ci::StopEnvironmentsService, services: true do
+ let(:project) { create(:project, :private) }
+ let(:user) { create(:user) }
+
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when environment with review app exists' do
+ before do
+ create(:environment, :with_review_app, project: project,
+ ref: 'feature')
+ end
+
+ context 'when user has permission to stop environment' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when environment is associated with removed branch' do
+ it 'stops environment' do
+ expect_environment_stopped_on('feature')
+ end
+ end
+
+ context 'when environment is associated with different branch' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+
+ context 'when specified branch does not exist' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('non/existent/branch')
+ end
+ end
+
+ context 'when no branch not specified' do
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on(nil)
+ end
+ end
+
+ context 'when environment is not stoppable' do
+ before do
+ allow_any_instance_of(Environment)
+ .to receive(:stoppable?).and_return(false)
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('feature')
+ end
+ end
+ end
+
+ context 'when user does not have permission to stop environment' do
+ before do
+ project.team << [user, :guest]
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+ end
+
+ context 'when there is no environment associated with review app' do
+ before do
+ create(:environment, project: project)
+ end
+
+ context 'when user has permission to stop environments' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'does not stop environment' do
+ expect_environment_not_stopped_on('master')
+ end
+ end
+ end
+
+ context 'when environment does not exist' do
+ it 'does not raise error' do
+ expect { service.execute('master') }
+ .not_to raise_error
+ end
+ end
+ end
+
+ def expect_environment_stopped_on(branch)
+ expect_any_instance_of(Environment)
+ .to receive(:stop!)
+
+ service.execute(branch)
+ end
+
+ def expect_environment_not_stopped_on(branch)
+ expect_any_instance_of(Environment)
+ .not_to receive(:stop!)
+
+ service.execute(branch)
+ end
+end
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
new file mode 100644
index 00000000000..336f5dafb5b
--- /dev/null
+++ b/spec/services/delete_branch_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe DeleteBranchService, services: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when user has access to push to repository' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'removes the branch' do
+ expect(branch_exists?('feature')).to be true
+
+ result = service.execute('feature')
+
+ expect(result[:status]).to eq :success
+ expect(branch_exists?('feature')).to be false
+ end
+ end
+
+ context 'when user does not have access to push to repository' do
+ it 'does not remove branch' do
+ expect(branch_exists?('feature')).to be true
+
+ result = service.execute('feature')
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq 'You dont have push access to repo'
+ expect(branch_exists?('feature')).to be true
+ end
+ end
+ end
+
+ def branch_exists?(branch_name)
+ repository.ref_exists?("refs/heads/#{branch_name}")
+ end
+end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
new file mode 100644
index 00000000000..181488e89c7
--- /dev/null
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesService, services: true do
+ subject(:service) { described_class.new(project, project.owner) }
+
+ let(:project) { create(:project) }
+
+ context '#execute' do
+ context 'unprotected branches' do
+ before do
+ service.execute
+ end
+
+ it 'deletes a branch that was merged' do
+ expect(project.repository.branch_names).not_to include('improve/awesome')
+ end
+
+ it 'keeps branch that is unmerged' do
+ expect(project.repository.branch_names).to include('feature')
+ end
+
+ it 'keeps "master"' do
+ expect(project.repository.branch_names).to include('master')
+ end
+ end
+
+ context 'protected branches' do
+ before do
+ create(:protected_branch, name: 'improve/awesome', project: project)
+ service.execute
+ end
+
+ it 'keeps protected branch' do
+ expect(project.repository.branch_names).to include('improve/awesome')
+ end
+ end
+
+ context 'user without rights' do
+ let(:user) { create(:user) }
+
+ it 'cannot execute' do
+ expect { described_class.new(project, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+
+ context '#async_execute' do
+ it 'calls DeleteMergedBranchesWorker async' do
+ expect(DeleteMergedBranchesWorker).to receive(:perform_async)
+
+ service.async_execute
+ end
+ end
+end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index da724643604..538e85cdc89 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe DestroyGroupService, services: true do
+ include DatabaseConnectionHelpers
+
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, namespace: group) }
@@ -50,6 +52,44 @@ describe DestroyGroupService, services: true do
describe 'asynchronous delete' do
it_behaves_like 'group destruction', true
+
+ context 'potential race conditions' do
+ context "when the `GroupDestroyWorker` task runs immediately" do
+ it "deletes the group" do
+ # Commit the contents of this spec's transaction so far
+ # so subsequent db connections can see it.
+ #
+ # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
+ # transaction is currently in progress". Without this, this
+ # spec will always be green, since the group created in setup
+ # cannot be seen by any other connections / threads in this spec.
+ Group.connection.commit_db_transaction
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).not_to be_nil
+
+ # Execute the contents of `GroupDestroyWorker` in a separate thread, to
+ # simulate data manipulation by the Sidekiq worker (different database
+ # connection / transaction).
+ expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
+ Thread.new { m[group_id, user_id] }.join(5)
+ end
+
+ # Kick off the initial group destroy in a new thread, so that
+ # it doesn't share this spec's database transaction.
+ Thread.new { DestroyGroupService.new(group, user).async_execute }.join(5)
+
+ group_record = run_with_new_database_connection do |conn|
+ conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
+ end
+
+ expect(group_record).to be_nil
+ end
+ end
+ end
end
describe 'synchronous delete' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 45bc44ba172..62f9982e840 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -302,6 +302,9 @@ describe GitPushService, services: true do
author_email: commit_author.email
)
+ allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ and_return(commit)
+
allow(project.repository).to receive(:commits_between).and_return([commit])
end
@@ -357,6 +360,9 @@ describe GitPushService, services: true do
committed_date: commit_time
)
+ allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ and_return(commit)
+
allow(project.repository).to receive(:commits_between).and_return([commit])
end
@@ -393,6 +399,9 @@ describe GitPushService, services: true do
allow(project.repository).to receive(:commits_between).
and_return([closing_commit])
+ allow_any_instance_of(ProcessCommitWorker).to receive(:find_commit).
+ and_return(closing_commit)
+
project.team << [commit_author, :master]
end
@@ -481,7 +490,17 @@ describe GitPushService, services: true do
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
- let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+ let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
+
+ before do
+ open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
+
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-1")
+ end
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
@@ -538,9 +557,16 @@ describe GitPushService, services: true do
let(:housekeeping) { Projects::HousekeepingService.new(project) }
before do
+ # Flush any raw Redis data stored by the housekeeping code.
+ Gitlab::Redis.with { |conn| conn.flushall }
+
allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping)
end
+ after do
+ Gitlab::Redis.with { |conn| conn.flushall }
+ end
+
it 'does not perform housekeeping when not needed' do
expect(housekeeping).not_to receive(:execute)
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index 6f7ce8ca992..5f3020b6525 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -260,14 +260,14 @@ describe Issuable::BulkUpdateService, services: true do
it 'subscribes the given user' do
bulk_update(issues, subscription_event: 'subscribe')
- expect(issues).to all(be_subscribed(user))
+ expect(issues).to all(be_subscribed(user, project))
end
end
describe 'unsubscribe from issues' do
let(:issues) do
create_list(:closed_issue, 2, project: project) do |issue|
- issue.subscriptions.create(user: user, subscribed: true)
+ issue.subscriptions.create(user: user, project: project, subscribed: true)
end
end
@@ -275,7 +275,7 @@ describe Issuable::BulkUpdateService, services: true do
bulk_update(issues, subscription_event: 'unsubscribe')
issues.each do |issue|
- expect(issue).not_to be_subscribed(user)
+ expect(issue).not_to be_subscribed(user, project)
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 5dfb33f4b28..4465f22a001 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -15,10 +15,39 @@ describe Issues::CloseService, services: true do
end
describe '#execute' do
+ let(:service) { described_class.new(project, user) }
+
+ it 'checks if the user is authorized to update the issue' do
+ expect(service).to receive(:can?).with(user, :update_issue, issue).
+ and_call_original
+
+ service.execute(issue)
+ end
+
+ it 'does not close the issue when the user is not authorized to do so' do
+ allow(service).to receive(:can?).with(user, :update_issue, issue).
+ and_return(false)
+
+ expect(service).not_to receive(:close_issue)
+ expect(service.execute(issue)).to eq(issue)
+ end
+
+ it 'closes the issue when the user is authorized to do so' do
+ allow(service).to receive(:can?).with(user, :update_issue, issue).
+ and_return(true)
+
+ expect(service).to receive(:close_issue).
+ with(issue, commit: nil, notifications: true, system_note: true)
+
+ service.execute(issue)
+ end
+ end
+
+ describe '#close_issue' do
context "valid params" do
before do
perform_enqueued_jobs do
- described_class.new(project, user).execute(issue)
+ described_class.new(project, user).close_issue(issue)
end
end
@@ -41,24 +70,12 @@ describe Issues::CloseService, services: true do
end
end
- context 'current user is not authorized to close issue' do
- before do
- perform_enqueued_jobs do
- described_class.new(project, guest).execute(issue)
- end
- end
-
- it 'does not close the issue' do
- expect(issue).to be_open
- end
- end
-
context 'when issue is not confidential' do
it 'executes issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
- described_class.new(project, user).execute(issue)
+ described_class.new(project, user).close_issue(issue)
end
end
@@ -69,14 +86,14 @@ describe Issues::CloseService, services: true do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
- described_class.new(project, user).execute(issue)
+ described_class.new(project, user).close_issue(issue)
end
end
context 'external issue tracker' do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
- described_class.new(project, user).execute(issue)
+ described_class.new(project, user).close_issue(issue)
end
it { expect(issue).to be_valid }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 1638a46ed51..4777a90639e 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -215,7 +215,7 @@ describe Issues::UpdateService, services: true do
let!(:subscriber) do
create(:user).tap do |u|
- label.toggle_subscription(u)
+ label.toggle_subscription(u, project)
project.team << [u, :developer]
end
end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 7b090343a3e..7d5a66801db 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Members::ApproveAccessRequestService, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:group) { create(:group, :public, :access_requestable) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 9995f3488af..574df6e0f42 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -26,6 +26,7 @@ describe Members::DestroyService, services: true do
context 'when the given member is an access requester' do
before do
source.members.find_by(user_id: member_user).destroy
+ source.update_attributes(request_access_enabled: true)
source.request_access(member_user)
end
let(:access_requester) { source.requesters.find_by(user_id: member_user) }
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 0d2d5f03199..853c125dadb 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -2,8 +2,6 @@ require 'spec_helper'
describe Members::RequestAccessService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
- let(:group) { create(:group, :private) }
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
@@ -31,27 +29,26 @@ describe Members::RequestAccessService, services: true do
end
context 'when current user cannot request access to the project' do
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :private) }
+ end
end
+ end
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ context 'when access requests are disabled' do
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { create(source_type, :public) }
+ end
end
end
context 'when current user can request access to the project' do
- before do
- project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { project }
- end
-
- it_behaves_like 'a service creating a access request' do
- let(:source) { group }
+ %i[project group].each do |source_type|
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { create(source_type, :public, :access_requestable) }
+ end
end
end
end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index dd656c3bbb7..a44312dd363 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -1,13 +1,22 @@
require 'spec_helper'
-# Write specs in this file.
describe MergeRequests::AddTodoWhenBuildFailsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { create(:project) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
- let(:pipeline) { create(:ci_pipeline_with_one_job, ref: merge_request.source_branch, project: project, sha: sha) }
- let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user, commit_message: 'Awesome message') }
+ let(:ref) { merge_request.source_branch }
+
+ let(:pipeline) do
+ create(:ci_pipeline_with_one_job, ref: ref,
+ project: project,
+ sha: sha)
+ end
+
+ let(:service) do
+ described_class.new(project, user, commit_message: 'Awesome message')
+ end
+
let(:todo_service) { TodoService.new }
let(:merge_request) do
@@ -23,7 +32,9 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
describe '#execute' do
context 'commit status with ref' do
- let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch, pipeline: pipeline) }
+ let(:commit_status) do
+ create(:generic_commit_status, ref: ref, pipeline: pipeline)
+ end
it 'notifies the todo service' do
expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
@@ -32,7 +43,7 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
end
context 'commit status with non-HEAD ref' do
- let(:commit_status) { create(:generic_commit_status, ref: merge_request.source_branch) }
+ let(:commit_status) { create(:generic_commit_status, ref: ref) }
it 'does not notify the todo service' do
expect(todo_service).not_to receive(:merge_request_build_failed)
@@ -48,6 +59,18 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.execute(commit_status)
end
end
+
+ context 'when commit status is a build allowed to fail' do
+ let(:commit_status) do
+ create(:ci_build, :allowed_to_fail, ref: ref, pipeline: pipeline)
+ end
+
+ it 'does not create todo' do
+ expect(todo_service).not_to receive(:merge_request_build_failed)
+
+ service.execute(commit_status)
+ end
+ end
end
describe '#close' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 3a71776e81f..08829e4be70 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -4,8 +4,8 @@ describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public) }
let(:service) { MergeRequests::GetUrlsService.new(project) }
let(:source_branch) { "my_branch" }
- let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
- let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
+ let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
@@ -115,7 +115,7 @@ describe MergeRequests::GetUrlsService do
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
- let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
it 'returns 2 urls for both creating new and showing merge request' do
result = service.execute(changes)
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index f93d7732a9a..7db32a33c93 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -59,25 +59,43 @@ describe MergeRequests::MergeService, services: true do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
+ let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+ let(:commit) { double('commit', safe_message: "Fixes #{jira_issue.to_reference}") }
before do
project.update_attributes!(has_external_issue_tracker: true)
jira_service_settings
+ stub_jira_urls(jira_issue.id)
+ allow(merge_request).to receive(:commits).and_return([commit])
end
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
- expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+ expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, an_instance_of(JIRA::Resource::Issue)).once
service.execute(merge_request)
end
+ context "when jira_issue_transition_id is not present" do
+ before { allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) }
+
+ it "does not close issue" do
+ allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil)
+
+ expect_any_instance_of(JiraService).not_to receive(:transition_issue)
+
+ service.execute(merge_request)
+ end
+ end
+
context "wrong issue markdown" do
it 'does not close issues on JIRA issue tracker' do
- jira_issue = ExternalIssue.new('#123', project)
+ jira_issue = ExternalIssue.new('#JIRA-123', project)
+ stub_jira_urls(jira_issue)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index e515bc9f89c..0220f7e1db2 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -227,6 +227,16 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'when the source branch is deleted' do
+ it 'does not create a MergeRequestDiff record' do
+ refresh_service = service.new(@project, @user)
+
+ expect do
+ refresh_service.execute(@oldrev, Gitlab::Git::BLANK_SHA, 'refs/heads/master')
+ end.not_to change { MergeRequestDiff.count }
+ 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 2433a7dad06..cb5d7cdb467 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -199,7 +199,7 @@ describe MergeRequests::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
- let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+ let!(:subscriber) { create(:user) { |u| label.toggle_subscription(u, project) } }
before do
project.team << [non_subscriber, :developer]
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 93885c84dc3..25804696d2e 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -14,12 +14,41 @@ describe Notes::CreateService, services: true do
end
context "valid params" do
- before do
- @note = Notes::CreateService.new(project, user, opts).execute
+ it 'returns a valid note' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_valid
+ end
+
+ it 'returns a persisted note' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note).to be_persisted
+ end
+
+ it 'note has valid content' do
+ note = Notes::CreateService.new(project, user, opts).execute
+
+ expect(note.note).to eq(opts[:note])
end
- it { expect(@note).to be_valid }
- it { expect(@note.note).to eq(opts[:note]) }
+ it 'TodoService#new_note is called' do
+ note = build(:note)
+ allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+
+ expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
+
+ Notes::CreateService.new(project, user, opts).execute
+ end
+
+ it 'enqueues NewNoteWorker' do
+ note = build(:note, id: 999)
+ allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
+
+ expect(NewNoteWorker).to receive(:perform_async).with(note.id)
+
+ Notes::CreateService.new(project, user, opts).execute
+ end
end
describe 'note with commands' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 699b9925b4e..8726c9eaa55 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -17,7 +17,7 @@ describe NotificationService, services: true do
it 'sends no emails when no new mentions are present' do
send_notifications
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
it 'emails new mentions with a watch level higher than participant' do
@@ -27,7 +27,7 @@ describe NotificationService, services: true do
it 'does not email new mentions with a watch level equal to or less than participant' do
send_notifications(@u_participating, @u_mentioned)
- expect(ActionMailer::Base.deliveries).to be_empty
+ should_not_email_anyone
end
end
@@ -79,7 +79,7 @@ describe NotificationService, services: true do
# Ensure create SentNotification by noteable = issue 6 times, not noteable = note
expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_note(note)
@@ -111,7 +111,7 @@ describe NotificationService, services: true do
context 'participating' do
context 'by note' do
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
note.author = @u_lazy_participant
note.save
notification.new_note(note)
@@ -134,7 +134,7 @@ describe NotificationService, services: true do
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global!
update_custom_notification(:new_note, @u_custom_global)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
it do
@@ -173,7 +173,7 @@ describe NotificationService, services: true do
expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_note(note)
@@ -196,7 +196,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
note.project.team << [note.author, :master]
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#new_note' do
@@ -238,7 +238,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
note.project.team << [note.author, :master]
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#new_note' do
@@ -273,7 +273,7 @@ describe NotificationService, services: true do
before do
build_team(note.project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, project)
update_custom_notification(:new_note, @u_custom_global)
@@ -342,13 +342,15 @@ describe NotificationService, services: true do
end
describe 'Issues' do
- let(:project) { create(:empty_project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:another_project) { create(:empty_project, :public, namespace: group) }
let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' }
before do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
update_custom_notification(:new_issue, @u_custom_global)
end
@@ -377,13 +379,24 @@ describe NotificationService, services: true do
end
it "emails subscribers of the issue's labels" do
- subscriber = create(:user)
- label = create(:label, issues: [issue])
+ user_1 = create(:user)
+ user_2 = create(:user)
+ user_3 = create(:user)
+ user_4 = create(:user)
+ label = create(:label, project: project, issues: [issue])
+ group_label = create(:group_label, group: group, issues: [issue])
issue.reload
- label.toggle_subscription(subscriber)
+ label.toggle_subscription(user_1, project)
+ group_label.toggle_subscription(user_2, project)
+ group_label.toggle_subscription(user_3, another_project)
+ group_label.toggle_subscription(user_4)
+
notification.new_issue(issue, @u_disabled)
- should_email(subscriber)
+ should_email(user_1)
+ should_email(user_2)
+ should_not_email(user_3)
+ should_email(user_4)
end
context 'confidential issues' do
@@ -399,16 +412,16 @@ describe NotificationService, services: true do
project.team << [member, :developer]
project.team << [guest, :guest]
- label = create(:label, issues: [confidential_issue])
+ label = create(:label, project: project, issues: [confidential_issue])
confidential_issue.reload
- label.toggle_subscription(non_member)
- label.toggle_subscription(author)
- label.toggle_subscription(assignee)
- label.toggle_subscription(member)
- label.toggle_subscription(guest)
- label.toggle_subscription(admin)
+ label.toggle_subscription(non_member, project)
+ label.toggle_subscription(author, project)
+ label.toggle_subscription(assignee, project)
+ label.toggle_subscription(member, project)
+ label.toggle_subscription(guest, project)
+ label.toggle_subscription(admin, project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.new_issue(confidential_issue, @u_disabled)
@@ -554,20 +567,30 @@ describe NotificationService, services: true do
end
describe '#relabeled_issue' do
- let(:label) { create(:label, issues: [issue]) }
- let(:label2) { create(:label) }
- let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
- let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+ let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', issues: [issue]) }
+ let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let(:label_1) { create(:label, project: project, title: 'Label 1', issues: [issue]) }
+ let(:label_2) { create(:label, project: project, title: 'Label 2') }
+ let!(:subscriber_to_group_label_1) { create(:user) { |u| group_label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_1_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u, project) } }
+ let!(:subscriber_2_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u) } }
+ let!(:subscriber_to_group_label_2_on_another_project) { create(:user) { |u| group_label_2.toggle_subscription(u, another_project) } }
+ let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the issue's added labels only" do
- notification.relabeled_issue(issue, [label2], @u_disabled)
-
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
+
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
- notification.relabeled_issue(issue, [label2], @u_disabled)
+ notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
should_not_email(issue.assignee)
should_not_email(issue.author)
@@ -578,8 +601,12 @@ describe NotificationService, services: true do
should_not_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
context 'confidential issues' do
@@ -590,21 +617,21 @@ describe NotificationService, services: true do
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
- let!(:label_1) { create(:label, issues: [confidential_issue]) }
- let!(:label_2) { create(:label) }
+ let!(:label_1) { create(:label, project: project, issues: [confidential_issue]) }
+ let!(:label_2) { create(:label, project: project) }
it "emails subscribers of the issue's labels that can read the issue" do
project.team << [member, :developer]
project.team << [guest, :guest]
- label_2.toggle_subscription(non_member)
- label_2.toggle_subscription(author)
- label_2.toggle_subscription(assignee)
- label_2.toggle_subscription(member)
- label_2.toggle_subscription(guest)
- label_2.toggle_subscription(admin)
+ label_2.toggle_subscription(non_member, project)
+ label_2.toggle_subscription(author, project)
+ label_2.toggle_subscription(assignee, project)
+ label_2.toggle_subscription(member, project)
+ label_2.toggle_subscription(guest, project)
+ label_2.toggle_subscription(admin, project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
@@ -725,7 +752,9 @@ describe NotificationService, services: true do
end
describe 'Merge Requests' do
- let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:another_project) { create(:empty_project, :public, namespace: group) }
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
@@ -733,7 +762,7 @@ describe NotificationService, services: true do
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, project)
update_custom_notification(:new_merge_request, @u_custom_global)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#new_merge_request' do
@@ -758,12 +787,23 @@ describe NotificationService, services: true do
end
it "emails subscribers of the merge request's labels" do
- subscriber = create(:user)
- label = create(:label, merge_requests: [merge_request])
- label.toggle_subscription(subscriber)
+ user_1 = create(:user)
+ user_2 = create(:user)
+ user_3 = create(:user)
+ user_4 = create(:user)
+ label = create(:label, project: project, merge_requests: [merge_request])
+ group_label = create(:group_label, group: group, merge_requests: [merge_request])
+ label.toggle_subscription(user_1, project)
+ group_label.toggle_subscription(user_2, project)
+ group_label.toggle_subscription(user_3, another_project)
+ group_label.toggle_subscription(user_4)
+
notification.new_merge_request(merge_request, @u_disabled)
- should_email(subscriber)
+ should_email(user_1)
+ should_email(user_2)
+ should_not_email(user_3)
+ should_email(user_4)
end
context 'participating' do
@@ -857,20 +897,30 @@ describe NotificationService, services: true do
end
describe '#relabel_merge_request' do
- let(:label) { create(:label, merge_requests: [merge_request]) }
- let(:label2) { create(:label) }
- let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } }
- let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } }
+ let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', merge_requests: [merge_request]) }
+ let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
+ let(:label_1) { create(:label, project: project, title: 'Label 1', merge_requests: [merge_request]) }
+ let(:label_2) { create(:label, project: project, title: 'Label 2') }
+ let!(:subscriber_to_group_label_1) { create(:user) { |u| group_label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_1_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u, project) } }
+ let!(:subscriber_2_to_group_label_2) { create(:user) { |u| group_label_2.toggle_subscription(u) } }
+ let!(:subscriber_to_group_label_2_on_another_project) { create(:user) { |u| group_label_2.toggle_subscription(u, another_project) } }
+ let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } }
+ let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } }
it "emails subscribers of the merge request's added labels only" do
- notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
-
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
+
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
it "doesn't send email to anyone but subscribers of the given labels" do
- notification.relabeled_merge_request(merge_request, [label2], @u_disabled)
+ notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled)
should_not_email(merge_request.assignee)
should_not_email(merge_request.author)
@@ -881,8 +931,12 @@ describe NotificationService, services: true do
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_lazy_participant)
- should_not_email(subscriber_to_label)
- should_email(subscriber_to_label2)
+ should_not_email(subscriber_to_label_1)
+ should_not_email(subscriber_to_group_label_1)
+ should_not_email(subscriber_to_group_label_2_on_another_project)
+ should_email(subscriber_1_to_group_label_2)
+ should_email(subscriber_2_to_group_label_2)
+ should_email(subscriber_to_label_2)
end
end
@@ -1111,7 +1165,7 @@ describe NotificationService, services: true do
before do
build_team(project)
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
describe '#project_was_moved' do
@@ -1290,10 +1344,10 @@ describe NotificationService, services: true do
project.team << [@unsubscriber, :master]
project.team << [@watcher_and_subscriber, :master]
- issuable.subscriptions.create(user: @subscriber, subscribed: true)
- issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
- issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
+ issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
+ issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
# Make the watcher a subscriber to detect dupes
- issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
+ issuable.subscriptions.create(user: @watcher_and_subscriber, project: project, subscribed: true)
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 876bfaf085c..fbd22560d6e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -10,13 +10,6 @@ describe Projects::CreateService, services: true do
}
end
- it 'creates services on Project creation' do
- project = create_project(@user, @opts)
- project.reload
-
- expect(project.services).not_to be_empty
- end
-
it 'creates labels on Project creation if there are templates' do
Label.create(title: "bug", template: true)
project = create_project(@user, @opts)
@@ -41,6 +34,8 @@ describe Projects::CreateService, services: true do
@group = create :group
@group.add_owner(@user)
+ @user.refresh_authorized_projects # Ensure cache is warm
+
@opts.merge!(namespace_id: @group.id)
@project = create_project(@user, @opts)
end
@@ -48,6 +43,7 @@ describe Projects::CreateService, services: true do
it { expect(@project).to be_valid }
it { expect(@project.owner).to eq(@group) }
it { expect(@project.namespace).to eq(@group) }
+ it { expect(@user.authorized_projects).to include(@project) }
end
context 'error handling' do
@@ -137,6 +133,19 @@ describe Projects::CreateService, services: true do
expect(project.namespace).to eq(@user.namespace)
end
end
+
+ context 'when there is an active service template' do
+ before do
+ create(:service, project: nil, template: true, active: true)
+ end
+
+ it 'creates a service from this template' do
+ project = create_project(@user, @opts)
+ project.reload
+
+ expect(project.services.count).to eq 1
+ end
+ end
end
def create_project(user, opts)
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index b57e338b782..becf627a4f5 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -169,7 +169,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unsubscribe command' do
it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
- issuable.subscribe(developer)
+ issuable.subscribe(developer, project)
_, updates = service.execute(content, issuable)
expect(updates).to eq(subscription_event: 'unsubscribe')
@@ -321,7 +321,7 @@ describe SlashCommands::InterpretService, services: true do
it_behaves_like 'multiple label with same argument' do
let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
let(:issuable) { issue }
- end
+ end
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5bb107fdd85..150e21574a1 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SystemNoteService, services: true do
+ include Gitlab::Routing.url_helpers
+
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
@@ -534,7 +536,7 @@ describe SystemNoteService, services: true do
let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
- let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit }
@@ -543,6 +545,30 @@ describe SystemNoteService, services: true do
before { stub_jira_urls(jira_issue.id) }
+ noteable_types = ["merge_requests", "commit"]
+
+ noteable_types.each do |type|
+ context "when noteable is a #{type}" do
+ it "blocks cross reference when #{type.underscore}_events is false" do
+ jira_tracker.update("#{type}_events" => false)
+
+ noteable = type == "commit" ? commit : merge_request
+ result = described_class.cross_reference(jira_issue, noteable, author)
+
+ expect(result).to eq("Events for #{noteable.class.to_s.underscore.humanize.pluralize.downcase} are disabled.")
+ end
+
+ it "blocks cross reference when #{type.underscore}_events is true" do
+ jira_tracker.update("#{type}_events" => true)
+
+ noteable = type == "commit" ? commit : merge_request
+ result = described_class.cross_reference(jira_issue, noteable, author)
+
+ expect(result).to eq(success_message)
+ end
+ end
+ end
+
context 'in JIRA issue tracker' do
before { jira_service_settings }
@@ -550,16 +576,48 @@ describe SystemNoteService, services: true do
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.to eq(success_message) }
+
+ it "creates remote link" do
+ subject
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_commit_url(project.namespace, project, commit),
+ title: "GitLab: Mentioned on commit - #{commit.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
end
end
- context 'issue from an issue' do
+ context 'in commit' do
context 'in JIRA issue tracker' do
before { jira_service_settings }
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(success_message) }
+
+ it "creates remote link" do
+ subject
+
+ expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)).with(
+ body: hash_including(
+ GlobalID: "GitLab",
+ object: {
+ url: namespace_project_issue_url(project.namespace, project, issue),
+ title: "GitLab: Mentioned on issue - #{issue.title}",
+ icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ status: { resolved: false }
+ }
+ )
+ ).once
+ end
end
end
@@ -572,6 +630,13 @@ describe SystemNoteService, services: true do
subject { described_class.cross_reference(jira_issue, commit, author) }
it { is_expected.not_to eq(success_message) }
+
+ it 'does not try to create comment and remote link' do
+ subject
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue))
+ expect(WebMock).not_to have_requested(:post, jira_api_remote_link_url(jira_issue))
+ end
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b2ca856f89f..bead1a006d1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,9 +26,11 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
config.include Warden::Test::Helpers, type: :request
- config.include LoginHelpers, type: :feature
+ config.include LoginHelpers, type: :feature
+ config.include SearchHelpers, type: :feature
config.include StubConfiguration
config.include EmailHelpers
config.include TestEnv
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 62a5b46d47b..75c95d70951 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -49,7 +49,8 @@ module CycleAnalyticsHelpers
end
def merge_merge_requests_closing_issue(issue)
- merge_requests = issue.closed_by_merge_requests
+ merge_requests = issue.closed_by_merge_requests(user)
+
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
diff --git a/spec/support/database_connection_helpers.rb b/spec/support/database_connection_helpers.rb
new file mode 100644
index 00000000000..763329499f0
--- /dev/null
+++ b/spec/support/database_connection_helpers.rb
@@ -0,0 +1,9 @@
+module DatabaseConnectionHelpers
+ def run_with_new_database_connection
+ pool = ActiveRecord::Base.connection_pool
+ conn = pool.checkout
+ yield conn
+ ensure
+ pool.checkin(conn)
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index ac38e31b77e..247f0954221 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -11,6 +11,10 @@ RSpec.configure do |config|
DatabaseCleaner.strategy = :truncation
end
+ config.before(:each, truncate: true) do
+ DatabaseCleaner.strategy = :truncation
+ end
+
config.before(:each) do
DatabaseCleaner.start
end
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index 0bfc4685532..3e979f2f470 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -1,23 +1,33 @@
module EmailHelpers
- def sent_to_user?(user)
- ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ def sent_to_user?(user, recipients = email_recipients)
+ recipients.include?(user.notification_email)
end
def reset_delivered_emails!
ActionMailer::Base.deliveries.clear
end
- def should_only_email(*users)
- users.each {|user| should_email(user) }
- recipients = ActionMailer::Base.deliveries.flat_map(&:to)
+ def should_only_email(*users, kind: :to)
+ recipients = email_recipients(kind: kind)
+
+ users.each { |user| should_email(user, recipients) }
+
expect(recipients.count).to eq(users.count)
end
- def should_email(user)
- expect(sent_to_user?(user)).to be_truthy
+ def should_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_truthy
+ end
+
+ def should_not_email(user, recipients = email_recipients)
+ expect(sent_to_user?(user, recipients)).to be_falsey
+ end
+
+ def should_not_email_anyone
+ expect(ActionMailer::Base.deliveries).to be_empty
end
- def should_not_email(user)
- expect(sent_to_user?(user)).to be_falsey
+ def email_recipients(kind: :to)
+ ActionMailer::Base.deliveries.flat_map(&kind)
end
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 5e3b8f2b23e..194620d0a68 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -230,31 +230,31 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note subscribing to the #{issuable_type}" do
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
end
end
context "with a note unsubscribing to the #{issuable_type} as done" do
before do
- issuable.subscribe(master)
+ issuable.subscribe(master, project)
end
it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(master)).to be_truthy
+ expect(issuable.subscribed?(master, project)).to be_truthy
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
- expect(issuable.subscribed?(master)).to be_falsy
+ expect(issuable.subscribed?(master, project)).to be_falsy
end
end
end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 96e0dad6b55..929fc0c5182 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -6,7 +6,8 @@ module JiraServiceHelper
properties = {
title: "JIRA tracker",
url: JIRA_URL,
- project_key: "JIRA"
+ project_key: "JIRA",
+ jira_issue_transition_id: '1'
}
jira_tracker.update_attributes(properties: properties, active: true)
@@ -57,6 +58,10 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}/comment"
end
+ def jira_api_remote_link_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/remotelink"
+ end
+
def jira_api_transition_url(issue_id)
JIRA_API + "/issue/#{issue_id}/transitions"
end
@@ -75,6 +80,7 @@ module JiraServiceHelper
WebMock.stub_request(:get, jira_issue_url(issue_id))
WebMock.stub_request(:get, jira_api_test_url)
WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_remote_link_url(issue_id))
WebMock.stub_request(:post, jira_api_transition_url(issue_id))
end
end
diff --git a/spec/support/matchers/be_url.rb b/spec/support/matchers/be_url.rb
new file mode 100644
index 00000000000..f8096af1b22
--- /dev/null
+++ b/spec/support/matchers/be_url.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :be_url do |_|
+ match do |actual|
+ URI.parse(actual) rescue false
+ end
+end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 3956d05060b..49867aa5cc4 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -7,7 +7,7 @@ shared_context 'gitlab email notification' do
let(:new_user_address) { 'newguy@example.com' }
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/project_features_apply_to_issuables_shared_examples.rb
new file mode 100644
index 00000000000..4621d17549b
--- /dev/null
+++ b/spec/support/project_features_apply_to_issuables_shared_examples.rb
@@ -0,0 +1,56 @@
+shared_examples 'project features apply to issuables' do |klass|
+ let(:described_class) { klass }
+
+ let(:group) { create(:group) }
+ let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user }
+ let(:user_outside_group) { create(:user) }
+
+ let(:project) { create(:empty_project, :public, project_args) }
+
+ def project_args
+ feature = "#{described_class.model_name.plural}_access_level".to_sym
+
+ args = { group: group }
+ args[feature] = access_level
+
+ args
+ end
+
+ before do
+ _ = issuable
+ login_as(user)
+ visit path
+ end
+
+ context 'public access level' do
+ let(:access_level) { ProjectFeature::ENABLED }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+ end
+
+ context 'private access level' do
+ let(:access_level) { ProjectFeature::PRIVATE }
+
+ context 'group member' do
+ let(:user) { user_in_group }
+
+ it { expect(page).to have_content(issuable.title) }
+ end
+
+ context 'non-member' do
+ let(:user) { user_outside_group }
+
+ it { expect(page).not_to have_content(issuable.title) }
+ end
+ end
+end
diff --git a/spec/support/reference_parser_shared_examples.rb b/spec/support/reference_parser_shared_examples.rb
new file mode 100644
index 00000000000..8eb74635a60
--- /dev/null
+++ b/spec/support/reference_parser_shared_examples.rb
@@ -0,0 +1,43 @@
+RSpec.shared_examples "referenced feature visibility" do |*related_features|
+ let(:feature_fields) do
+ related_features.map { |feature| (feature + "_access_level").to_sym }
+ end
+
+ before { link['data-project'] = project.id.to_s }
+
+ context "when feature is disabled" do
+ it "does not create reference" do
+ set_features_fields_to(ProjectFeature::DISABLED)
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([])
+ end
+ end
+
+ context "when feature is enabled only for team members" do
+ before { set_features_fields_to(ProjectFeature::PRIVATE) }
+
+ it "does not create reference for non member" do
+ non_member = create(:user)
+
+ expect(subject.nodes_visible_to_user(non_member, [link])).to eq([])
+ end
+
+ it "creates reference for member" do
+ project.team << [user, :developer]
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ context "when feature is enabled" do
+ # The project is public
+ it "creates reference" do
+ set_features_fields_to(ProjectFeature::ENABLED)
+
+ expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
+ end
+ end
+
+ def set_features_fields_to(visibility_level)
+ feature_fields.each { |field| project.project_feature.update_attribute(field, visibility_level) }
+ end
+end
diff --git a/spec/support/search_helpers.rb b/spec/support/search_helpers.rb
new file mode 100644
index 00000000000..abbbb636d66
--- /dev/null
+++ b/spec/support/search_helpers.rb
@@ -0,0 +1,5 @@
+module SearchHelpers
+ def select_filter(name)
+ find(:xpath, "//ul[contains(@class, 'search-filter')]//a[contains(.,'#{name}')]").click
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 778e665500d..4cf81be3adc 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -23,6 +23,7 @@ module TestEnv
'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455',
+ 'symlink-expand-diff' => '81e6355',
'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
@@ -34,6 +35,7 @@ module TestEnv
'conflict-missing-side' => 'eb227b3',
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
+ 'deleted-image-test' => '6c17798'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
new file mode 100644
index 00000000000..538ff952bf4
--- /dev/null
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -0,0 +1,51 @@
+require 'rake_helper'
+
+describe 'gitlab:ldap:check rake task' do
+ include LdapHelpers
+
+ before do
+ Rake.application.rake_require 'tasks/gitlab/check'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ context 'when LDAP is not enabled' do
+ it 'does not attempt to bind or search for users' do
+ expect(Gitlab::LDAP::Config).not_to receive(:providers)
+ expect(Gitlab::LDAP::Adapter).not_to receive(:open)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+ end
+
+ context 'when LDAP is enabled' do
+ let(:ldap) { double(:ldap) }
+ let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+ before do
+ allow(Gitlab::LDAP::Config)
+ .to receive_messages(
+ enabled?: true,
+ providers: ['ldapmain']
+ )
+ allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter)
+ allow(adapter).to receive(:users).and_return([])
+ end
+
+ it 'attempts to bind using credentials' do
+ stub_ldap_config(has_auth?: true)
+
+ expect(ldap).to receive(:bind)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+
+ it 'searches for 100 LDAP users' do
+ stub_ldap_config(uid: 'uid')
+
+ expect(adapter).to receive(:users).with('uid', '*', 100)
+
+ run_rake_task('gitlab:ldap:check')
+ end
+ end
+end
diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/builds/_build.html.haml_spec.rb
new file mode 100644
index 00000000000..e141a117731
--- /dev/null
+++ b/spec/views/projects/builds/_build.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/ci/builds/_build' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'won\'t include a column with a link to its pipeline by default' do
+ render partial: 'projects/ci/builds/build', locals: { build: build }
+
+ expect(rendered).not_to have_link('#1337')
+ expect(rendered).not_to have_text('#1337 by API')
+ end
+
+ it 'can include a column with a link to its pipeline' do
+ render partial: 'projects/ci/builds/build', locals: { build: build, pipeline_link: true }
+
+ expect(rendered).to have_link('#1337')
+ expect(rendered).to have_text('#1337 by API')
+ end
+end
diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
new file mode 100644
index 00000000000..49b20e5b36b
--- /dev/null
+++ b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/generic_commit_statuses/_generic_commit_status.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:generic_commit_status) { create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'won\'t include a column with a link to its pipeline by default' do
+ render partial: 'projects/generic_commit_statuses/generic_commit_status', locals: { generic_commit_status: generic_commit_status }
+
+ expect(rendered).not_to have_link('#1337')
+ expect(rendered).not_to have_text('#1337 by API')
+ end
+
+ it 'can include a column with a link to its pipeline' do
+ render partial: 'projects/generic_commit_statuses/generic_commit_status', locals: { generic_commit_status: generic_commit_status, pipeline_link: true }
+
+ expect(rendered).to have_link('#1337')
+ expect(rendered).to have_text('#1337 by API')
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index da43622d3f9..e0c77201116 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
-describe 'projects/builds/show' do
- include Devise::Test::ControllerHelpers
-
+describe 'projects/builds/show', :view do
let(:project) { create(:project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
let(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id)
+ create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:build) { create(:ci_build, pipeline: pipeline) }
before do
assign(:build, build)
@@ -17,6 +15,129 @@ describe 'projects/builds/show' do
allow(view).to receive(:can?).and_return(true)
end
+ describe 'environment info in build view' do
+ context 'build with latest deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging')
+ end
+
+ before do
+ create(:environment, name: 'staging')
+ create(:deployment, deployable: build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is the most recent deployment'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build with outdated deployment' do
+ let(:build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:second_build) do
+ create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
+ end
+
+ let(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ let!(:first_deployment) do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
+ let!(:second_deployment) do
+ create(:deployment, environment: environment, deployable: second_build)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is an out-of-date deployment ' \
+ "to staging.\nView the most recent deployment ##{second_deployment.iid}."
+ render
+
+ expect(rendered).to have_css('.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build failed to deploy' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed.'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build will deploy' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build that failed to deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'The deployment of this build to staging did not succeed'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+
+ context 'build that will deploy and environment has not been created' do
+ let(:build) do
+ create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
+ end
+
+ let!(:environment) do
+ create(:environment, name: 'staging', project: project)
+ end
+
+ it 'shows deployment message' do
+ expected_text = 'This build is creating a deployment to staging'
+ render
+
+ expect(rendered).to have_css(
+ '.environment-information', text: expected_text)
+ end
+ end
+ end
+
context 'when build is running' do
before do
build.run!
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
new file mode 100644
index 00000000000..18a1aab766c
--- /dev/null
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe AuthorizedProjectsWorker do
+ describe '#perform' do
+ it "refreshes user's authorized projects" do
+ user = create(:user)
+
+ expect(User).to receive(:find_by).with(id: user.id).and_return(user)
+ expect(user).to receive(:refresh_authorized_projects)
+
+ described_class.new.perform(user.id)
+ end
+
+ context "when user is not found" do
+ it "does nothing" do
+ expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
+
+ described_class.new.perform(999_999)
+ end
+ end
+ end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 788b92c1b84..a1aa336361a 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -24,7 +24,7 @@ describe BuildEmailWorker do
end
it "gracefully handles an input SMTP error" do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
subject.perform(build.id, [user.email], data.stringify_keys)
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
new file mode 100644
index 00000000000..d9497bd486c
--- /dev/null
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DeleteMergedBranchesWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { create(:project) }
+
+ describe "#perform" do
+ it "calls DeleteMergedBranchesService" do
+ expect_any_instance_of(DeleteMergedBranchesService).to receive(:execute).and_return(true)
+
+ worker.perform(project.id, project.owner.id)
+ end
+
+ it "returns false when project was not found" do
+ expect(worker.perform('unknown', project.owner.id)).to be_falsy
+ end
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 036d037f3f9..fc652f6f4c3 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -87,7 +87,7 @@ describe EmailsOnPushWorker do
context "when there is an SMTP error" do
before do
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
allow(subject).to receive_message_chain(:logger, :info)
perform
@@ -112,7 +112,7 @@ describe EmailsOnPushWorker do
original.call(Mail.new(mail.encoded))
end
- ActionMailer::Base.deliveries.clear
+ reset_delivered_emails!
end
it "sends the mail to each of the recipients" do
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
new file mode 100644
index 00000000000..8fdbb35afd0
--- /dev/null
+++ b/spec/workers/new_note_worker_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe NewNoteWorker do
+ context 'when Note found' do
+ let(:note) { create(:note) }
+
+ it "calls NotificationService#new_note" do
+ expect_any_instance_of(NotificationService).to receive(:new_note).with(note)
+
+ described_class.new.perform(note.id)
+ end
+
+ it "calls Notes::PostProcessService#execute" do
+ notes_post_process_service = double(Notes::PostProcessService)
+ allow(Notes::PostProcessService).to receive(:new).with(note) { notes_post_process_service }
+
+ expect(notes_post_process_service).to receive(:execute)
+
+ described_class.new.perform(note.id)
+ end
+ end
+
+ context 'when Note not found' do
+ let(:unexistent_note_id) { 999 }
+
+ it 'logs NewNoteWorker process skipping' do
+ expect(Rails.logger).to receive(:error).
+ with("NewNoteWorker: couldn't find note with ID=999, skipping job")
+
+ described_class.new.perform(unexistent_note_id)
+ end
+
+ it 'does not raise errors' do
+ expect { described_class.new.perform(unexistent_note_id) }.not_to raise_error
+ end
+
+ it "does not call NotificationService#new_note" do
+ expect_any_instance_of(NotificationService).not_to receive(:new_note)
+
+ described_class.new.perform(unexistent_note_id)
+ end
+
+ it "does not call Notes::PostProcessService#execute" do
+ expect_any_instance_of(Notes::PostProcessService).not_to receive(:execute)
+
+ described_class.new.perform(unexistent_note_id)
+ end
+ end
+end
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 2c9e7c2cd02..2d47d93acec 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -15,32 +15,36 @@ describe PipelineMetricsWorker do
end
describe '#perform' do
- subject { described_class.new.perform(pipeline.id) }
+ before do
+ described_class.new.perform(pipeline.id)
+ end
context 'when pipeline is running' do
let(:status) { 'running' }
it 'records the build start time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_started_at).to be_like_time(pipeline.started_at)
end
it 'clears the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
context 'when pipeline succeeded' do
let(:status) { 'success' }
it 'records the build end time' do
- subject
-
expect(merge_request.reload.metrics.latest_build_finished_at).to be_like_time(pipeline.finished_at)
end
+
+ it 'records the pipeline' do
+ expect(merge_request.reload.metrics.pipeline).to eq(pipeline)
+ end
end
end
end
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
new file mode 100644
index 00000000000..d487a719680
--- /dev/null
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -0,0 +1,131 @@
+require 'spec_helper'
+
+describe PipelineNotificationWorker do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('master').sha,
+ user: pusher,
+ status: status)
+ end
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:pusher) { user }
+ let(:watcher) { pusher }
+
+ describe '#execute' do
+ before do
+ reset_delivered_emails!
+ pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
+ end
+
+ context 'when watcher has developer access' do
+ before do
+ pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
+ end
+
+ shared_examples 'sending emails' do
+ it 'sends emails' do
+ perform_enqueued_jobs do
+ subject.perform(pipeline.id)
+ end
+
+ emails = ActionMailer::Base.deliveries
+ actual = emails.flat_map(&:bcc).sort
+ expected_receivers = receivers.map(&:email).uniq.sort
+
+ expect(actual).to eq(expected_receivers)
+ expect(emails.size).to eq(1)
+ expect(emails.last.subject).to include(email_subject)
+ end
+ end
+
+ context 'with success pipeline' do
+ let(:status) { 'success' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
+ let(:receivers) { [pusher, watcher] }
+
+ it_behaves_like 'sending emails'
+
+ context 'with pipeline from someone else' do
+ let(:pusher) { create(:user) }
+ let(:watcher) { user }
+
+ context 'with success pipeline notification on' do
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', success_pipeline: true)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with success pipeline notification off' do
+ let(:receivers) { [pusher] }
+
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', success_pipeline: false)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+
+ it_behaves_like 'sending emails'
+
+ context 'with pipeline from someone else' do
+ let(:pusher) { create(:user) }
+ let(:watcher) { user }
+
+ context 'with failed pipeline notification on' do
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+
+ context 'with failed pipeline notification off' do
+ let(:receivers) { [pusher] }
+
+ before do
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: false)
+ end
+
+ it_behaves_like 'sending emails'
+ end
+ end
+ end
+ end
+ end
+
+ context 'when watcher has no read_build access' do
+ let(:status) { 'failed' }
+ let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
+ let(:watcher) { create(:user) }
+
+ before do
+ pipeline.project.team << [watcher, Gitlab::Access::GUEST]
+
+ watcher.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true)
+
+ perform_enqueued_jobs do
+ subject.perform(pipeline.id)
+ end
+ end
+
+ it 'does not send emails' do
+ should_only_email(pusher, kind: :bcc)
+ end
+ end
+ end
+end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
new file mode 100644
index 00000000000..3e4fee42240
--- /dev/null
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe ProcessCommitWorker do
+ let(:worker) { described_class.new }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:commit) { project.commit }
+
+ describe '#perform' do
+ it 'does not process the commit when the project does not exist' do
+ expect(worker).not_to receive(:close_issues)
+
+ worker.perform(-1, user.id, commit.id)
+ end
+
+ it 'does not process the commit when the user does not exist' do
+ expect(worker).not_to receive(:close_issues)
+
+ worker.perform(project.id, -1, commit.id)
+ end
+
+ it 'does not process the commit when the commit no longer exists' do
+ expect(worker).not_to receive(:close_issues)
+
+ worker.perform(project.id, user.id, 'this-should-does-not-exist')
+ end
+
+ it 'processes the commit message' do
+ expect(worker).to receive(:process_commit_message).and_call_original
+
+ worker.perform(project.id, user.id, commit.id)
+ end
+
+ it 'updates the issue metrics' do
+ expect(worker).to receive(:update_issue_metrics).and_call_original
+
+ worker.perform(project.id, user.id, commit.id)
+ end
+ end
+
+ describe '#process_commit_message' do
+ context 'when pushing to the default branch' do
+ it 'closes issues that should be closed per the commit message' do
+ allow(commit).to receive(:safe_message).
+ and_return("Closes #{issue.to_reference}")
+
+ expect(worker).to receive(:close_issues).
+ with(project, user, user, commit, [issue])
+
+ worker.process_commit_message(project, commit, user, user, true)
+ end
+ end
+
+ context 'when pushing to a non-default branch' do
+ it 'does not close any issues' do
+ allow(commit).to receive(:safe_message).
+ and_return("Closes #{issue.to_reference}")
+
+ expect(worker).not_to receive(:close_issues)
+
+ worker.process_commit_message(project, commit, user, user, false)
+ end
+ end
+
+ it 'creates cross references' do
+ expect(commit).to receive(:create_cross_references!)
+
+ worker.process_commit_message(project, commit, user, user)
+ end
+ end
+
+ describe '#close_issues' do
+ context 'when the user can update the issues' do
+ it 'closes the issues' do
+ worker.close_issues(project, user, user, commit, [issue])
+
+ issue.reload
+
+ expect(issue.closed?).to eq(true)
+ end
+ end
+
+ context 'when the user can not update the issues' do
+ it 'does not close the issues' do
+ other_user = create(:user)
+
+ worker.close_issues(project, other_user, other_user, commit, [issue])
+
+ issue.reload
+
+ expect(issue.closed?).to eq(false)
+ end
+ end
+ end
+
+ describe '#update_issue_metrics' do
+ it 'updates any existing issue metrics' do
+ allow(commit).to receive(:safe_message).
+ and_return("Closes #{issue.to_reference}")
+
+ worker.update_issue_metrics(commit, user)
+
+ metric = Issue::Metrics.first
+
+ expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
new file mode 100644
index 00000000000..19e6c13a655
--- /dev/null
+++ b/vendor/assets/javascripts/es6-promise.auto.js
@@ -0,0 +1,1159 @@
+/*!
+ * @overview es6-promise - a tiny implementation of Promises/A+.
+ * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
+ * @license Licensed under MIT license
+ * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
+ * @version 4.0.5
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.ES6Promise = factory());
+}(this, (function () { 'use strict';
+
+function objectOrFunction(x) {
+ return typeof x === 'function' || typeof x === 'object' && x !== null;
+}
+
+function isFunction(x) {
+ return typeof x === 'function';
+}
+
+var _isArray = undefined;
+if (!Array.isArray) {
+ _isArray = function (x) {
+ return Object.prototype.toString.call(x) === '[object Array]';
+ };
+} else {
+ _isArray = Array.isArray;
+}
+
+var isArray = _isArray;
+
+var len = 0;
+var vertxNext = undefined;
+var customSchedulerFn = undefined;
+
+var asap = function asap(callback, arg) {
+ queue[len] = callback;
+ queue[len + 1] = arg;
+ len += 2;
+ if (len === 2) {
+ // If len is 2, that means that we need to schedule an async flush.
+ // If additional callbacks are queued before the queue is flushed, they
+ // will be processed by this flush that we are scheduling.
+ if (customSchedulerFn) {
+ customSchedulerFn(flush);
+ } else {
+ scheduleFlush();
+ }
+ }
+};
+
+function setScheduler(scheduleFn) {
+ customSchedulerFn = scheduleFn;
+}
+
+function setAsap(asapFn) {
+ asap = asapFn;
+}
+
+var browserWindow = typeof window !== 'undefined' ? window : undefined;
+var browserGlobal = browserWindow || {};
+var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
+var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]';
+
+// test for web worker but not in IE10
+var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
+
+// node
+function useNextTick() {
+ // node version 0.10.x displays a deprecation warning when nextTick is used recursively
+ // see https://github.com/cujojs/when/issues/410 for details
+ return function () {
+ return process.nextTick(flush);
+ };
+}
+
+// vertx
+function useVertxTimer() {
+ if (typeof vertxNext !== 'undefined') {
+ return function () {
+ vertxNext(flush);
+ };
+ }
+
+ return useSetTimeout();
+}
+
+function useMutationObserver() {
+ var iterations = 0;
+ var observer = new BrowserMutationObserver(flush);
+ var node = document.createTextNode('');
+ observer.observe(node, { characterData: true });
+
+ return function () {
+ node.data = iterations = ++iterations % 2;
+ };
+}
+
+// web worker
+function useMessageChannel() {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = flush;
+ return function () {
+ return channel.port2.postMessage(0);
+ };
+}
+
+function useSetTimeout() {
+ // Store setTimeout reference so es6-promise will be unaffected by
+ // other code modifying setTimeout (like sinon.useFakeTimers())
+ var globalSetTimeout = setTimeout;
+ return function () {
+ return globalSetTimeout(flush, 1);
+ };
+}
+
+var queue = new Array(1000);
+function flush() {
+ for (var i = 0; i < len; i += 2) {
+ var callback = queue[i];
+ var arg = queue[i + 1];
+
+ callback(arg);
+
+ queue[i] = undefined;
+ queue[i + 1] = undefined;
+ }
+
+ len = 0;
+}
+
+function attemptVertx() {
+ try {
+ var r = require;
+ var vertx = r('vertx');
+ vertxNext = vertx.runOnLoop || vertx.runOnContext;
+ return useVertxTimer();
+ } catch (e) {
+ return useSetTimeout();
+ }
+}
+
+var scheduleFlush = undefined;
+// Decide what async method to use to triggering processing of queued callbacks:
+if (isNode) {
+ scheduleFlush = useNextTick();
+} else if (BrowserMutationObserver) {
+ scheduleFlush = useMutationObserver();
+} else if (isWorker) {
+ scheduleFlush = useMessageChannel();
+} else if (browserWindow === undefined && typeof require === 'function') {
+ scheduleFlush = attemptVertx();
+} else {
+ scheduleFlush = useSetTimeout();
+}
+
+function then(onFulfillment, onRejection) {
+ var _arguments = arguments;
+
+ var parent = this;
+
+ var child = new this.constructor(noop);
+
+ if (child[PROMISE_ID] === undefined) {
+ makePromise(child);
+ }
+
+ var _state = parent._state;
+
+ if (_state) {
+ (function () {
+ var callback = _arguments[_state - 1];
+ asap(function () {
+ return invokeCallback(_state, child, callback, parent._result);
+ });
+ })();
+ } else {
+ subscribe(parent, child, onFulfillment, onRejection);
+ }
+
+ return child;
+}
+
+/**
+ `Promise.resolve` returns a promise that will become resolved with the
+ passed `value`. It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ resolve(1);
+ });
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.resolve(1);
+
+ promise.then(function(value){
+ // value === 1
+ });
+ ```
+
+ @method resolve
+ @static
+ @param {Any} value value that the returned promise will be resolved with
+ Useful for tooling.
+ @return {Promise} a promise that will become fulfilled with the given
+ `value`
+*/
+function resolve(object) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (object && typeof object === 'object' && object.constructor === Constructor) {
+ return object;
+ }
+
+ var promise = new Constructor(noop);
+ _resolve(promise, object);
+ return promise;
+}
+
+var PROMISE_ID = Math.random().toString(36).substring(16);
+
+function noop() {}
+
+var PENDING = void 0;
+var FULFILLED = 1;
+var REJECTED = 2;
+
+var GET_THEN_ERROR = new ErrorObject();
+
+function selfFulfillment() {
+ return new TypeError("You cannot resolve a promise with itself");
+}
+
+function cannotReturnOwn() {
+ return new TypeError('A promises callback cannot return that same promise.');
+}
+
+function getThen(promise) {
+ try {
+ return promise.then;
+ } catch (error) {
+ GET_THEN_ERROR.error = error;
+ return GET_THEN_ERROR;
+ }
+}
+
+function tryThen(then, value, fulfillmentHandler, rejectionHandler) {
+ try {
+ then.call(value, fulfillmentHandler, rejectionHandler);
+ } catch (e) {
+ return e;
+ }
+}
+
+function handleForeignThenable(promise, thenable, then) {
+ asap(function (promise) {
+ var sealed = false;
+ var error = tryThen(then, thenable, function (value) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+ if (thenable !== value) {
+ _resolve(promise, value);
+ } else {
+ fulfill(promise, value);
+ }
+ }, function (reason) {
+ if (sealed) {
+ return;
+ }
+ sealed = true;
+
+ _reject(promise, reason);
+ }, 'Settle: ' + (promise._label || ' unknown promise'));
+
+ if (!sealed && error) {
+ sealed = true;
+ _reject(promise, error);
+ }
+ }, promise);
+}
+
+function handleOwnThenable(promise, thenable) {
+ if (thenable._state === FULFILLED) {
+ fulfill(promise, thenable._result);
+ } else if (thenable._state === REJECTED) {
+ _reject(promise, thenable._result);
+ } else {
+ subscribe(thenable, undefined, function (value) {
+ return _resolve(promise, value);
+ }, function (reason) {
+ return _reject(promise, reason);
+ });
+ }
+}
+
+function handleMaybeThenable(promise, maybeThenable, then$$) {
+ if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) {
+ handleOwnThenable(promise, maybeThenable);
+ } else {
+ if (then$$ === GET_THEN_ERROR) {
+ _reject(promise, GET_THEN_ERROR.error);
+ } else if (then$$ === undefined) {
+ fulfill(promise, maybeThenable);
+ } else if (isFunction(then$$)) {
+ handleForeignThenable(promise, maybeThenable, then$$);
+ } else {
+ fulfill(promise, maybeThenable);
+ }
+ }
+}
+
+function _resolve(promise, value) {
+ if (promise === value) {
+ _reject(promise, selfFulfillment());
+ } else if (objectOrFunction(value)) {
+ handleMaybeThenable(promise, value, getThen(value));
+ } else {
+ fulfill(promise, value);
+ }
+}
+
+function publishRejection(promise) {
+ if (promise._onerror) {
+ promise._onerror(promise._result);
+ }
+
+ publish(promise);
+}
+
+function fulfill(promise, value) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+
+ promise._result = value;
+ promise._state = FULFILLED;
+
+ if (promise._subscribers.length !== 0) {
+ asap(publish, promise);
+ }
+}
+
+function _reject(promise, reason) {
+ if (promise._state !== PENDING) {
+ return;
+ }
+ promise._state = REJECTED;
+ promise._result = reason;
+
+ asap(publishRejection, promise);
+}
+
+function subscribe(parent, child, onFulfillment, onRejection) {
+ var _subscribers = parent._subscribers;
+ var length = _subscribers.length;
+
+ parent._onerror = null;
+
+ _subscribers[length] = child;
+ _subscribers[length + FULFILLED] = onFulfillment;
+ _subscribers[length + REJECTED] = onRejection;
+
+ if (length === 0 && parent._state) {
+ asap(publish, parent);
+ }
+}
+
+function publish(promise) {
+ var subscribers = promise._subscribers;
+ var settled = promise._state;
+
+ if (subscribers.length === 0) {
+ return;
+ }
+
+ var child = undefined,
+ callback = undefined,
+ detail = promise._result;
+
+ for (var i = 0; i < subscribers.length; i += 3) {
+ child = subscribers[i];
+ callback = subscribers[i + settled];
+
+ if (child) {
+ invokeCallback(settled, child, callback, detail);
+ } else {
+ callback(detail);
+ }
+ }
+
+ promise._subscribers.length = 0;
+}
+
+function ErrorObject() {
+ this.error = null;
+}
+
+var TRY_CATCH_ERROR = new ErrorObject();
+
+function tryCatch(callback, detail) {
+ try {
+ return callback(detail);
+ } catch (e) {
+ TRY_CATCH_ERROR.error = e;
+ return TRY_CATCH_ERROR;
+ }
+}
+
+function invokeCallback(settled, promise, callback, detail) {
+ var hasCallback = isFunction(callback),
+ value = undefined,
+ error = undefined,
+ succeeded = undefined,
+ failed = undefined;
+
+ if (hasCallback) {
+ value = tryCatch(callback, detail);
+
+ if (value === TRY_CATCH_ERROR) {
+ failed = true;
+ error = value.error;
+ value = null;
+ } else {
+ succeeded = true;
+ }
+
+ if (promise === value) {
+ _reject(promise, cannotReturnOwn());
+ return;
+ }
+ } else {
+ value = detail;
+ succeeded = true;
+ }
+
+ if (promise._state !== PENDING) {
+ // noop
+ } else if (hasCallback && succeeded) {
+ _resolve(promise, value);
+ } else if (failed) {
+ _reject(promise, error);
+ } else if (settled === FULFILLED) {
+ fulfill(promise, value);
+ } else if (settled === REJECTED) {
+ _reject(promise, value);
+ }
+}
+
+function initializePromise(promise, resolver) {
+ try {
+ resolver(function resolvePromise(value) {
+ _resolve(promise, value);
+ }, function rejectPromise(reason) {
+ _reject(promise, reason);
+ });
+ } catch (e) {
+ _reject(promise, e);
+ }
+}
+
+var id = 0;
+function nextId() {
+ return id++;
+}
+
+function makePromise(promise) {
+ promise[PROMISE_ID] = id++;
+ promise._state = undefined;
+ promise._result = undefined;
+ promise._subscribers = [];
+}
+
+function Enumerator(Constructor, input) {
+ this._instanceConstructor = Constructor;
+ this.promise = new Constructor(noop);
+
+ if (!this.promise[PROMISE_ID]) {
+ makePromise(this.promise);
+ }
+
+ if (isArray(input)) {
+ this._input = input;
+ this.length = input.length;
+ this._remaining = input.length;
+
+ this._result = new Array(this.length);
+
+ if (this.length === 0) {
+ fulfill(this.promise, this._result);
+ } else {
+ this.length = this.length || 0;
+ this._enumerate();
+ if (this._remaining === 0) {
+ fulfill(this.promise, this._result);
+ }
+ }
+ } else {
+ _reject(this.promise, validationError());
+ }
+}
+
+function validationError() {
+ return new Error('Array Methods must be provided an Array');
+};
+
+Enumerator.prototype._enumerate = function () {
+ var length = this.length;
+ var _input = this._input;
+
+ for (var i = 0; this._state === PENDING && i < length; i++) {
+ this._eachEntry(_input[i], i);
+ }
+};
+
+Enumerator.prototype._eachEntry = function (entry, i) {
+ var c = this._instanceConstructor;
+ var resolve$$ = c.resolve;
+
+ if (resolve$$ === resolve) {
+ var _then = getThen(entry);
+
+ if (_then === then && entry._state !== PENDING) {
+ this._settledAt(entry._state, i, entry._result);
+ } else if (typeof _then !== 'function') {
+ this._remaining--;
+ this._result[i] = entry;
+ } else if (c === Promise) {
+ var promise = new c(noop);
+ handleMaybeThenable(promise, entry, _then);
+ this._willSettleAt(promise, i);
+ } else {
+ this._willSettleAt(new c(function (resolve$$) {
+ return resolve$$(entry);
+ }), i);
+ }
+ } else {
+ this._willSettleAt(resolve$$(entry), i);
+ }
+};
+
+Enumerator.prototype._settledAt = function (state, i, value) {
+ var promise = this.promise;
+
+ if (promise._state === PENDING) {
+ this._remaining--;
+
+ if (state === REJECTED) {
+ _reject(promise, value);
+ } else {
+ this._result[i] = value;
+ }
+ }
+
+ if (this._remaining === 0) {
+ fulfill(promise, this._result);
+ }
+};
+
+Enumerator.prototype._willSettleAt = function (promise, i) {
+ var enumerator = this;
+
+ subscribe(promise, undefined, function (value) {
+ return enumerator._settledAt(FULFILLED, i, value);
+ }, function (reason) {
+ return enumerator._settledAt(REJECTED, i, reason);
+ });
+};
+
+/**
+ `Promise.all` accepts an array of promises, and returns a new promise which
+ is fulfilled with an array of fulfillment values for the passed promises, or
+ rejected with the reason of the first passed promise to be rejected. It casts all
+ elements of the passed iterable to promises as it runs this algorithm.
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = resolve(2);
+ let promise3 = resolve(3);
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // The array here would be [ 1, 2, 3 ];
+ });
+ ```
+
+ If any of the `promises` given to `all` are rejected, the first promise
+ that is rejected will be given as an argument to the returned promises's
+ rejection handler. For example:
+
+ Example:
+
+ ```javascript
+ let promise1 = resolve(1);
+ let promise2 = reject(new Error("2"));
+ let promise3 = reject(new Error("3"));
+ let promises = [ promise1, promise2, promise3 ];
+
+ Promise.all(promises).then(function(array){
+ // Code here never runs because there are rejected promises!
+ }, function(error) {
+ // error.message === "2"
+ });
+ ```
+
+ @method all
+ @static
+ @param {Array} entries array of promises
+ @param {String} label optional string for labeling the promise.
+ Useful for tooling.
+ @return {Promise} promise that is fulfilled when all `promises` have been
+ fulfilled, or rejected if any of them become rejected.
+ @static
+*/
+function all(entries) {
+ return new Enumerator(this, entries).promise;
+}
+
+/**
+ `Promise.race` returns a new promise which is settled in the same way as the
+ first passed promise to settle.
+
+ Example:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 2');
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // result === 'promise 2' because it was resolved before promise1
+ // was resolved.
+ });
+ ```
+
+ `Promise.race` is deterministic in that only the state of the first
+ settled promise matters. For example, even if other promises given to the
+ `promises` array argument are resolved, but the first settled promise has
+ become rejected before the other promises became fulfilled, the returned
+ promise will become rejected:
+
+ ```javascript
+ let promise1 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ resolve('promise 1');
+ }, 200);
+ });
+
+ let promise2 = new Promise(function(resolve, reject){
+ setTimeout(function(){
+ reject(new Error('promise 2'));
+ }, 100);
+ });
+
+ Promise.race([promise1, promise2]).then(function(result){
+ // Code here never runs
+ }, function(reason){
+ // reason.message === 'promise 2' because promise 2 became rejected before
+ // promise 1 became fulfilled
+ });
+ ```
+
+ An example real-world use case is implementing timeouts:
+
+ ```javascript
+ Promise.race([ajax('foo.json'), timeout(5000)])
+ ```
+
+ @method race
+ @static
+ @param {Array} promises array of promises to observe
+ Useful for tooling.
+ @return {Promise} a promise which settles in the same way as the first passed
+ promise to settle.
+*/
+function race(entries) {
+ /*jshint validthis:true */
+ var Constructor = this;
+
+ if (!isArray(entries)) {
+ return new Constructor(function (_, reject) {
+ return reject(new TypeError('You must pass an array to race.'));
+ });
+ } else {
+ return new Constructor(function (resolve, reject) {
+ var length = entries.length;
+ for (var i = 0; i < length; i++) {
+ Constructor.resolve(entries[i]).then(resolve, reject);
+ }
+ });
+ }
+}
+
+/**
+ `Promise.reject` returns a promise rejected with the passed `reason`.
+ It is shorthand for the following:
+
+ ```javascript
+ let promise = new Promise(function(resolve, reject){
+ reject(new Error('WHOOPS'));
+ });
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ Instead of writing the above, your code now simply becomes the following:
+
+ ```javascript
+ let promise = Promise.reject(new Error('WHOOPS'));
+
+ promise.then(function(value){
+ // Code here doesn't run because the promise is rejected!
+ }, function(reason){
+ // reason.message === 'WHOOPS'
+ });
+ ```
+
+ @method reject
+ @static
+ @param {Any} reason value that the returned promise will be rejected with.
+ Useful for tooling.
+ @return {Promise} a promise rejected with the given `reason`.
+*/
+function reject(reason) {
+ /*jshint validthis:true */
+ var Constructor = this;
+ var promise = new Constructor(noop);
+ _reject(promise, reason);
+ return promise;
+}
+
+function needsResolver() {
+ throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
+}
+
+function needsNew() {
+ throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
+}
+
+/**
+ Promise objects represent the eventual result of an asynchronous operation. The
+ primary way of interacting with a promise is through its `then` method, which
+ registers callbacks to receive either a promise's eventual value or the reason
+ why the promise cannot be fulfilled.
+
+ Terminology
+ -----------
+
+ - `promise` is an object or function with a `then` method whose behavior conforms to this specification.
+ - `thenable` is an object or function that defines a `then` method.
+ - `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
+ - `exception` is a value that is thrown using the throw statement.
+ - `reason` is a value that indicates why a promise was rejected.
+ - `settled` the final resting state of a promise, fulfilled or rejected.
+
+ A promise can be in one of three states: pending, fulfilled, or rejected.
+
+ Promises that are fulfilled have a fulfillment value and are in the fulfilled
+ state. Promises that are rejected have a rejection reason and are in the
+ rejected state. A fulfillment value is never a thenable.
+
+ Promises can also be said to *resolve* a value. If this value is also a
+ promise, then the original promise's settled state will match the value's
+ settled state. So a promise that *resolves* a promise that rejects will
+ itself reject, and a promise that *resolves* a promise that fulfills will
+ itself fulfill.
+
+
+ Basic Usage:
+ ------------
+
+ ```js
+ let promise = new Promise(function(resolve, reject) {
+ // on success
+ resolve(value);
+
+ // on failure
+ reject(reason);
+ });
+
+ promise.then(function(value) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Advanced Usage:
+ ---------------
+
+ Promises shine when abstracting away asynchronous interactions such as
+ `XMLHttpRequest`s.
+
+ ```js
+ function getJSON(url) {
+ return new Promise(function(resolve, reject){
+ let xhr = new XMLHttpRequest();
+
+ xhr.open('GET', url);
+ xhr.onreadystatechange = handler;
+ xhr.responseType = 'json';
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.send();
+
+ function handler() {
+ if (this.readyState === this.DONE) {
+ if (this.status === 200) {
+ resolve(this.response);
+ } else {
+ reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
+ }
+ }
+ };
+ });
+ }
+
+ getJSON('/posts.json').then(function(json) {
+ // on fulfillment
+ }, function(reason) {
+ // on rejection
+ });
+ ```
+
+ Unlike callbacks, promises are great composable primitives.
+
+ ```js
+ Promise.all([
+ getJSON('/posts'),
+ getJSON('/comments')
+ ]).then(function(values){
+ values[0] // => postsJSON
+ values[1] // => commentsJSON
+
+ return values;
+ });
+ ```
+
+ @class Promise
+ @param {function} resolver
+ Useful for tooling.
+ @constructor
+*/
+function Promise(resolver) {
+ this[PROMISE_ID] = nextId();
+ this._result = this._state = undefined;
+ this._subscribers = [];
+
+ if (noop !== resolver) {
+ typeof resolver !== 'function' && needsResolver();
+ this instanceof Promise ? initializePromise(this, resolver) : needsNew();
+ }
+}
+
+Promise.all = all;
+Promise.race = race;
+Promise.resolve = resolve;
+Promise.reject = reject;
+Promise._setScheduler = setScheduler;
+Promise._setAsap = setAsap;
+Promise._asap = asap;
+
+Promise.prototype = {
+ constructor: Promise,
+
+ /**
+ The primary way of interacting with a promise is through its `then` method,
+ which registers callbacks to receive either a promise's eventual value or the
+ reason why the promise cannot be fulfilled.
+
+ ```js
+ findUser().then(function(user){
+ // user is available
+ }, function(reason){
+ // user is unavailable, and you are given the reason why
+ });
+ ```
+
+ Chaining
+ --------
+
+ The return value of `then` is itself a promise. This second, 'downstream'
+ promise is resolved with the return value of the first promise's fulfillment
+ or rejection handler, or rejected if the handler throws an exception.
+
+ ```js
+ findUser().then(function (user) {
+ return user.name;
+ }, function (reason) {
+ return 'default name';
+ }).then(function (userName) {
+ // If `findUser` fulfilled, `userName` will be the user's name, otherwise it
+ // will be `'default name'`
+ });
+
+ findUser().then(function (user) {
+ throw new Error('Found user, but still unhappy');
+ }, function (reason) {
+ throw new Error('`findUser` rejected and we're unhappy');
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
+ // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
+ });
+ ```
+ If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
+
+ ```js
+ findUser().then(function (user) {
+ throw new PedagogicalException('Upstream error');
+ }).then(function (value) {
+ // never reached
+ }).then(function (value) {
+ // never reached
+ }, function (reason) {
+ // The `PedgagocialException` is propagated all the way down to here
+ });
+ ```
+
+ Assimilation
+ ------------
+
+ Sometimes the value you want to propagate to a downstream promise can only be
+ retrieved asynchronously. This can be achieved by returning a promise in the
+ fulfillment or rejection handler. The downstream promise will then be pending
+ until the returned promise is settled. This is called *assimilation*.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // The user's comments are now available
+ });
+ ```
+
+ If the assimliated promise rejects, then the downstream promise will also reject.
+
+ ```js
+ findUser().then(function (user) {
+ return findCommentsByAuthor(user);
+ }).then(function (comments) {
+ // If `findCommentsByAuthor` fulfills, we'll have the value here
+ }, function (reason) {
+ // If `findCommentsByAuthor` rejects, we'll have the reason here
+ });
+ ```
+
+ Simple Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let result;
+
+ try {
+ result = findResult();
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+ findResult(function(result, err){
+ if (err) {
+ // failure
+ } else {
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findResult().then(function(result){
+ // success
+ }, function(reason){
+ // failure
+ });
+ ```
+
+ Advanced Example
+ --------------
+
+ Synchronous Example
+
+ ```javascript
+ let author, books;
+
+ try {
+ author = findAuthor();
+ books = findBooksByAuthor(author);
+ // success
+ } catch(reason) {
+ // failure
+ }
+ ```
+
+ Errback Example
+
+ ```js
+
+ function foundBooks(books) {
+
+ }
+
+ function failure(reason) {
+
+ }
+
+ findAuthor(function(author, err){
+ if (err) {
+ failure(err);
+ // failure
+ } else {
+ try {
+ findBoooksByAuthor(author, function(books, err) {
+ if (err) {
+ failure(err);
+ } else {
+ try {
+ foundBooks(books);
+ } catch(reason) {
+ failure(reason);
+ }
+ }
+ });
+ } catch(error) {
+ failure(err);
+ }
+ // success
+ }
+ });
+ ```
+
+ Promise Example;
+
+ ```javascript
+ findAuthor().
+ then(findBooksByAuthor).
+ then(function(books){
+ // found books
+ }).catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method then
+ @param {Function} onFulfilled
+ @param {Function} onRejected
+ Useful for tooling.
+ @return {Promise}
+ */
+ then: then,
+
+ /**
+ `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
+ as the catch block of a try/catch statement.
+
+ ```js
+ function findAuthor(){
+ throw new Error('couldn't find that author');
+ }
+
+ // synchronous
+ try {
+ findAuthor();
+ } catch(reason) {
+ // something went wrong
+ }
+
+ // async with promises
+ findAuthor().catch(function(reason){
+ // something went wrong
+ });
+ ```
+
+ @method catch
+ @param {Function} onRejection
+ Useful for tooling.
+ @return {Promise}
+ */
+ 'catch': function _catch(onRejection) {
+ return this.then(null, onRejection);
+ }
+};
+
+function polyfill() {
+ var local = undefined;
+
+ if (typeof global !== 'undefined') {
+ local = global;
+ } else if (typeof self !== 'undefined') {
+ local = self;
+ } else {
+ try {
+ local = Function('return this')();
+ } catch (e) {
+ throw new Error('polyfill failed because global object is unavailable in this environment');
+ }
+ }
+
+ var P = local.Promise;
+
+ if (P) {
+ var promiseToString = null;
+ try {
+ promiseToString = Object.prototype.toString.call(P.resolve());
+ } catch (e) {
+ // silently ignored
+ }
+
+ if (promiseToString === '[object Promise]' && !P.cast) {
+ return;
+ }
+ }
+
+ local.Promise = Promise;
+}
+
+// Strange compat..
+Promise.polyfill = polyfill;
+Promise.Promise = Promise;
+
+return Promise;
+
+})));
+
+ES6Promise.polyfill();
+//# sourceMappingURL=es6-promise.auto.map
diff --git a/vendor/assets/javascripts/jquery.timeago.js b/vendor/assets/javascripts/jquery.timeago.js
deleted file mode 100644
index de76cdd2ea7..00000000000
--- a/vendor/assets/javascripts/jquery.timeago.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/* eslint-disable */
-/**
- * Timeago is a jQuery plugin that makes it easy to support automatically
- * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
- *
- * @name timeago
- * @version 1.1.0
- * @requires jQuery v1.2.3+
- * @author Ryan McGeary
- * @license MIT License - http://www.opensource.org/licenses/mit-license.php
- *
- * For usage and examples, visit:
- * http://timeago.yarp.com/
- *
- * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
- */
-
-(function (factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module.
- define(['jquery'], factory);
- } else {
- // Browser globals
- factory(jQuery);
- }
-}(function ($) {
- $.timeago = function(timestamp) {
- if (timestamp instanceof Date) {
- return inWords(timestamp);
- } else if (typeof timestamp === "string") {
- return inWords($.timeago.parse(timestamp));
- } else if (typeof timestamp === "number") {
- return inWords(new Date(timestamp));
- } else {
- return inWords($.timeago.datetime(timestamp));
- }
- };
- var $t = $.timeago;
-
- $.extend($.timeago, {
- settings: {
- refreshMillis: 60000,
- allowFuture: false,
- strings: {
- prefixAgo: null,
- prefixFromNow: null,
- suffixAgo: "ago",
- suffixFromNow: "from now",
- seconds: "less than a minute",
- minute: "about a minute",
- minutes: "%d minutes",
- hour: "about an hour",
- hours: "about %d hours",
- day: "a day",
- days: "%d days",
- month: "about a month",
- months: "%d months",
- year: "about a year",
- years: "%d years",
- wordSeparator: " ",
- numbers: []
- }
- },
- inWords: function(distanceMillis) {
- var $l = this.settings.strings;
- var prefix = $l.prefixAgo;
- var suffix = $l.suffixAgo;
- if (this.settings.allowFuture) {
- if (distanceMillis < 0) {
- prefix = $l.prefixFromNow;
- suffix = $l.suffixFromNow;
- }
- }
-
- var seconds = Math.abs(distanceMillis) / 1000;
- var minutes = seconds / 60;
- var hours = minutes / 60;
- var days = hours / 24;
- var years = days / 365;
-
- function substitute(stringOrFunction, number) {
- var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
- var value = ($l.numbers && $l.numbers[number]) || number;
- return string.replace(/%d/i, value);
- }
-
- var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
- seconds < 90 && substitute($l.minute, 1) ||
- minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
- minutes < 90 && substitute($l.hour, 1) ||
- hours < 24 && substitute($l.hours, Math.round(hours)) ||
- hours < 42 && substitute($l.day, 1) ||
- days < 30 && substitute($l.days, Math.round(days)) ||
- days < 45 && substitute($l.month, 1) ||
- days < 365 && substitute($l.months, Math.round(days / 30)) ||
- years < 1.5 && substitute($l.year, 1) ||
- substitute($l.years, Math.round(years));
-
- var separator = $l.wordSeparator || "";
- if ($l.wordSeparator === undefined) { separator = " "; }
- return $.trim([prefix, words, suffix].join(separator));
- },
- parse: function(iso8601) {
- var s = $.trim(iso8601);
- s = s.replace(/\.\d+/,""); // remove milliseconds
- s = s.replace(/-/,"/").replace(/-/,"/");
- s = s.replace(/T/," ").replace(/Z/," UTC");
- s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
- return new Date(s);
- },
- datetime: function(elem) {
- var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
- return $t.parse(iso8601);
- },
- isTime: function(elem) {
- // jQuery's `is()` doesn't play well with HTML5 in IE
- return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
- }
- });
-
- // functions that can be called via $(el).timeago('action')
- // init is default when no action is given
- // functions are called with context of a single element
- var functions = {
- init: function(){
- var refresh_el = $.proxy(refresh, this);
- refresh_el();
- var $s = $t.settings;
- if ($s.refreshMillis > 0) {
- setInterval(refresh_el, $s.refreshMillis);
- }
- },
- update: function(time){
- $(this).data('timeago', { datetime: $t.parse(time) });
- refresh.apply(this);
- }
- };
-
- $.fn.timeago = function(action, options) {
- var fn = action ? functions[action] : functions.init;
- if(!fn){
- throw new Error("Unknown function name '"+ action +"' for timeago");
- }
- // each over objects here and call the requested function
- this.each(function(){
- fn.call(this, options);
- });
- return this;
- };
-
- function refresh() {
- var data = prepareData(this);
- if (!isNaN(data.datetime)) {
- $(this).text(inWords(data.datetime));
- }
- return this;
- }
-
- function prepareData(element) {
- element = $(element);
- if (!element.data("timeago")) {
- element.data("timeago", { datetime: $t.datetime(element) });
- var text = $.trim(element.text());
- if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
- element.attr("title", text);
- }
- }
- return element.data("timeago");
- }
-
- function inWords(date) {
- return $t.inWords(distance(date));
- }
-
- function distance(date) {
- return (new Date().getTime() - date.getTime());
- }
-
- // fix for IE6 suckage
- document.createElement("abbr");
- document.createElement("time");
-}));
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
index 7ae95897a01..ea15bfac416 100644
--- a/vendor/assets/javascripts/vue.full.js
+++ b/vendor/assets/javascripts/vue.full.js
@@ -1,10073 +1,7515 @@
/*!
- * Vue.js v1.0.26
- * (c) 2016 Evan You
+ * Vue.js v2.0.3
+ * (c) 2014-2016 Evan You
* Released under the MIT License.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
-}(this, function () { 'use strict';
+}(this, (function () { 'use strict';
- function set(obj, key, val) {
- if (hasOwn(obj, key)) {
- obj[key] = val;
- return;
- }
- if (obj._isVue) {
- set(obj._data, key, val);
- return;
- }
- var ob = obj.__ob__;
- if (!ob) {
- obj[key] = val;
- return;
- }
- ob.convert(key, val);
- ob.dep.notify();
- if (ob.vms) {
- var i = ob.vms.length;
- while (i--) {
- var vm = ob.vms[i];
- vm._proxy(key);
- vm._digest();
- }
- }
- return val;
- }
+/* */
- /**
- * Delete a property and trigger change if necessary.
- *
- * @param {Object} obj
- * @param {String} key
- */
+/**
+ * Convert a value to a string that is actually rendered.
+ */
+function _toString (val) {
+ return val == null
+ ? ''
+ : typeof val === 'object'
+ ? JSON.stringify(val, null, 2)
+ : String(val)
+}
+
+/**
+ * Convert a input value to a number for persistence.
+ * If the conversion fails, return original string.
+ */
+function toNumber (val) {
+ var n = parseFloat(val, 10);
+ return (n || n === 0) ? n : val
+}
+
+/**
+ * Make a map and return a function for checking if a key
+ * is in that map.
+ */
+function makeMap (
+ str,
+ expectsLowerCase
+) {
+ var map = Object.create(null);
+ var list = str.split(',');
+ for (var i = 0; i < list.length; i++) {
+ map[list[i]] = true;
+ }
+ return expectsLowerCase
+ ? function (val) { return map[val.toLowerCase()]; }
+ : function (val) { return map[val]; }
+}
+
+/**
+ * Check if a tag is a built-in tag.
+ */
+var isBuiltInTag = makeMap('slot,component', true);
- function del(obj, key) {
- if (!hasOwn(obj, key)) {
- return;
- }
- delete obj[key];
- var ob = obj.__ob__;
- if (!ob) {
- if (obj._isVue) {
- delete obj._data[key];
- obj._digest();
- }
- return;
- }
- ob.dep.notify();
- if (ob.vms) {
- var i = ob.vms.length;
- while (i--) {
- var vm = ob.vms[i];
- vm._unproxy(key);
- vm._digest();
- }
+/**
+ * Remove an item from an array
+ */
+function remove$1 (arr, item) {
+ if (arr.length) {
+ var index = arr.indexOf(item);
+ if (index > -1) {
+ return arr.splice(index, 1)
}
}
+}
- var hasOwnProperty = Object.prototype.hasOwnProperty;
- /**
- * Check whether the object has the property.
- *
- * @param {Object} obj
- * @param {String} key
- * @return {Boolean}
- */
+/**
+ * Check whether the object has the property.
+ */
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+function hasOwn (obj, key) {
+ return hasOwnProperty.call(obj, key)
+}
+
+/**
+ * Check if value is primitive
+ */
+function isPrimitive (value) {
+ return typeof value === 'string' || typeof value === 'number'
+}
- function hasOwn(obj, key) {
- return hasOwnProperty.call(obj, key);
+/**
+ * Create a cached version of a pure function.
+ */
+function cached (fn) {
+ var cache = Object.create(null);
+ return function cachedFn (str) {
+ var hit = cache[str];
+ return hit || (cache[str] = fn(str))
}
+}
- /**
- * Check if an expression is a literal value.
- *
- * @param {String} exp
- * @return {Boolean}
- */
+/**
+ * Camelize a hyphen-delmited string.
+ */
+var camelizeRE = /-(\w)/g;
+var camelize = cached(function (str) {
+ return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
+});
- var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
+/**
+ * Capitalize a string.
+ */
+var capitalize = cached(function (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+});
- function isLiteral(exp) {
- return literalValueRE.test(exp);
+/**
+ * Hyphenate a camelCase string.
+ */
+var hyphenateRE = /([^-])([A-Z])/g;
+var hyphenate = cached(function (str) {
+ return str
+ .replace(hyphenateRE, '$1-$2')
+ .replace(hyphenateRE, '$1-$2')
+ .toLowerCase()
+});
+
+/**
+ * Simple bind, faster than native
+ */
+function bind$1 (fn, ctx) {
+ function boundFn (a) {
+ var l = arguments.length;
+ return l
+ ? l > 1
+ ? fn.apply(ctx, arguments)
+ : fn.call(ctx, a)
+ : fn.call(ctx)
+ }
+ // record original fn length
+ boundFn._length = fn.length;
+ return boundFn
+}
+
+/**
+ * Convert an Array-like object to a real Array.
+ */
+function toArray (list, start) {
+ start = start || 0;
+ var i = list.length - start;
+ var ret = new Array(i);
+ while (i--) {
+ ret[i] = list[i + start];
+ }
+ return ret
+}
+
+/**
+ * Mix properties into target object.
+ */
+function extend (to, _from) {
+ for (var key in _from) {
+ to[key] = _from[key];
}
+ return to
+}
- /**
- * Check if a string starts with $ or _
- *
- * @param {String} str
- * @return {Boolean}
- */
+/**
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ */
+function isObject (obj) {
+ return obj !== null && typeof obj === 'object'
+}
- function isReserved(str) {
- var c = (str + '').charCodeAt(0);
- return c === 0x24 || c === 0x5F;
+/**
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ */
+var toString = Object.prototype.toString;
+var OBJECT_STRING = '[object Object]';
+function isPlainObject (obj) {
+ return toString.call(obj) === OBJECT_STRING
+}
+
+/**
+ * Merge an Array of Objects into a single Object.
+ */
+function toObject (arr) {
+ var res = {};
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i]) {
+ extend(res, arr[i]);
+ }
}
+ return res
+}
- /**
- * Guard text output, make sure undefined outputs
- * empty string
- *
- * @param {*} value
- * @return {String}
- */
+/**
+ * Perform no operation.
+ */
+function noop () {}
- function _toString(value) {
- return value == null ? '' : value.toString();
- }
+/**
+ * Always return false.
+ */
+var no = function () { return false; };
- /**
- * Check and convert possible numeric strings to numbers
- * before setting back to data
- *
- * @param {*} value
- * @return {*|Number}
- */
+/**
+ * Generate a static keys string from compiler modules.
+ */
+function genStaticKeys (modules) {
+ return modules.reduce(function (keys, m) {
+ return keys.concat(m.staticKeys || [])
+ }, []).join(',')
+}
+
+/**
+ * Check if two values are loosely equal - that is,
+ * if they are plain objects, do they have the same shape?
+ */
+function looseEqual (a, b) {
+ /* eslint-disable eqeqeq */
+ return a == b || (
+ isObject(a) && isObject(b)
+ ? JSON.stringify(a) === JSON.stringify(b)
+ : false
+ )
+ /* eslint-enable eqeqeq */
+}
- function toNumber(value) {
- if (typeof value !== 'string') {
- return value;
- } else {
- var parsed = Number(value);
- return isNaN(parsed) ? value : parsed;
- }
+function looseIndexOf (arr, val) {
+ for (var i = 0; i < arr.length; i++) {
+ if (looseEqual(arr[i], val)) { return i }
}
+ return -1
+}
- /**
- * Convert string boolean literals into real booleans.
- *
- * @param {*} value
- * @return {*|Boolean}
- */
-
- function toBoolean(value) {
- return value === 'true' ? true : value === 'false' ? false : value;
- }
+/* */
+var config = {
/**
- * Strip quotes from a string
- *
- * @param {String} str
- * @return {String | false}
+ * Option merge strategies (used in core/util/options)
*/
-
- function stripQuotes(str) {
- var a = str.charCodeAt(0);
- var b = str.charCodeAt(str.length - 1);
- return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str;
- }
+ optionMergeStrategies: Object.create(null),
/**
- * Camelize a hyphen-delmited string.
- *
- * @param {String} str
- * @return {String}
+ * Whether to suppress warnings.
*/
-
- var camelizeRE = /-(\w)/g;
-
- function camelize(str) {
- return str.replace(camelizeRE, toUpper);
- }
-
- function toUpper(_, c) {
- return c ? c.toUpperCase() : '';
- }
+ silent: false,
/**
- * Hyphenate a camelCase string.
- *
- * @param {String} str
- * @return {String}
+ * Whether to enable devtools
*/
-
- var hyphenateRE = /([a-z\d])([A-Z])/g;
-
- function hyphenate(str) {
- return str.replace(hyphenateRE, '$1-$2').toLowerCase();
- }
+ devtools: "development" !== 'production',
/**
- * Converts hyphen/underscore/slash delimitered names into
- * camelized classNames.
- *
- * e.g. my-component => MyComponent
- * some_else => SomeElse
- * some/comp => SomeComp
- *
- * @param {String} str
- * @return {String}
+ * Error handler for watcher errors
*/
-
- var classifyRE = /(?:^|[-_\/])(\w)/g;
-
- function classify(str) {
- return str.replace(classifyRE, toUpper);
- }
+ errorHandler: null,
/**
- * Simple bind, faster than native
- *
- * @param {Function} fn
- * @param {Object} ctx
- * @return {Function}
+ * Ignore certain custom elements
*/
-
- function bind(fn, ctx) {
- return function (a) {
- var l = arguments.length;
- return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx);
- };
- }
+ ignoredElements: null,
/**
- * Convert an Array-like object to a real Array.
- *
- * @param {Array-like} list
- * @param {Number} [start] - start index
- * @return {Array}
+ * Custom user key aliases for v-on
*/
-
- function toArray(list, start) {
- start = start || 0;
- var i = list.length - start;
- var ret = new Array(i);
- while (i--) {
- ret[i] = list[i + start];
- }
- return ret;
- }
+ keyCodes: Object.create(null),
/**
- * Mix properties into target object.
- *
- * @param {Object} to
- * @param {Object} from
+ * Check if a tag is reserved so that it cannot be registered as a
+ * component. This is platform-dependent and may be overwritten.
*/
-
- function extend(to, from) {
- var keys = Object.keys(from);
- var i = keys.length;
- while (i--) {
- to[keys[i]] = from[keys[i]];
- }
- return to;
- }
+ isReservedTag: no,
/**
- * Quick object check - this is primarily used to tell
- * Objects from primitive values when we know the value
- * is a JSON-compliant type.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Check if a tag is an unknown element.
+ * Platform-dependent.
*/
-
- function isObject(obj) {
- return obj !== null && typeof obj === 'object';
- }
+ isUnknownElement: no,
/**
- * Strict object type check. Only returns true
- * for plain JavaScript objects.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Get the namespace of an element
*/
-
- var toString = Object.prototype.toString;
- var OBJECT_STRING = '[object Object]';
-
- function isPlainObject(obj) {
- return toString.call(obj) === OBJECT_STRING;
- }
+ getTagNamespace: noop,
/**
- * Array type check.
- *
- * @param {*} obj
- * @return {Boolean}
+ * Check if an attribute must be bound using property, e.g. value
+ * Platform-dependent.
*/
-
- var isArray = Array.isArray;
+ mustUseProp: no,
/**
- * Define a property.
- *
- * @param {Object} obj
- * @param {String} key
- * @param {*} val
- * @param {Boolean} [enumerable]
+ * List of asset types that a component can own.
*/
-
- function def(obj, key, val, enumerable) {
- Object.defineProperty(obj, key, {
- value: val,
- enumerable: !!enumerable,
- writable: true,
- configurable: true
- });
- }
+ _assetTypes: [
+ 'component',
+ 'directive',
+ 'filter'
+ ],
/**
- * Debounce a function so it only gets called after the
- * input stops arriving after the given wait period.
- *
- * @param {Function} func
- * @param {Number} wait
- * @return {Function} - the debounced function
+ * List of lifecycle hooks.
*/
-
- function _debounce(func, wait) {
- var timeout, args, context, timestamp, result;
- var later = function later() {
- var last = Date.now() - timestamp;
- if (last < wait && last >= 0) {
- timeout = setTimeout(later, wait - last);
- } else {
- timeout = null;
- result = func.apply(context, args);
- if (!timeout) context = args = null;
- }
- };
- return function () {
- context = this;
- args = arguments;
- timestamp = Date.now();
- if (!timeout) {
- timeout = setTimeout(later, wait);
- }
- return result;
- };
- }
+ _lifecycleHooks: [
+ 'beforeCreate',
+ 'created',
+ 'beforeMount',
+ 'mounted',
+ 'beforeUpdate',
+ 'updated',
+ 'beforeDestroy',
+ 'destroyed',
+ 'activated',
+ 'deactivated'
+ ],
/**
- * Manual indexOf because it's slightly faster than
- * native.
- *
- * @param {Array} arr
- * @param {*} obj
+ * Max circular updates allowed in a scheduler flush cycle.
*/
-
- function indexOf(arr, obj) {
- var i = arr.length;
- while (i--) {
- if (arr[i] === obj) return i;
- }
- return -1;
- }
+ _maxUpdateCount: 100,
/**
- * Make a cancellable version of an async callback.
- *
- * @param {Function} fn
- * @return {Function}
+ * Server rendering?
*/
+ _isServer: "client" === 'server'
+};
- function cancellable(fn) {
- var cb = function cb() {
- if (!cb.cancelled) {
- return fn.apply(this, arguments);
- }
- };
- cb.cancel = function () {
- cb.cancelled = true;
- };
- return cb;
- }
+/* */
- /**
- * Check if two values are loosely equal - that is,
- * if they are plain objects, do they have the same shape?
- *
- * @param {*} a
- * @param {*} b
- * @return {Boolean}
- */
+/**
+ * Check if a string starts with $ or _
+ */
+function isReserved (str) {
+ var c = (str + '').charCodeAt(0);
+ return c === 0x24 || c === 0x5F
+}
- function looseEqual(a, b) {
- /* eslint-disable eqeqeq */
- return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false);
- /* eslint-enable eqeqeq */
+/**
+ * Define a property.
+ */
+function def (obj, key, val, enumerable) {
+ Object.defineProperty(obj, key, {
+ value: val,
+ enumerable: !!enumerable,
+ writable: true,
+ configurable: true
+ });
+}
+
+/**
+ * Parse simple path.
+ */
+var bailRE = /[^\w\.\$]/;
+function parsePath (path) {
+ if (bailRE.test(path)) {
+ return
+ } else {
+ var segments = path.split('.');
+ return function (obj) {
+ for (var i = 0; i < segments.length; i++) {
+ if (!obj) { return }
+ obj = obj[segments[i]];
+ }
+ return obj
+ }
}
+}
- var hasProto = ('__proto__' in {});
+/* */
+/* globals MutationObserver */
- // Browser environment sniffing
- var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]';
+// can we use __proto__?
+var hasProto = '__proto__' in {};
- // detect devtools
- var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
+// Browser environment sniffing
+var inBrowser =
+ typeof window !== 'undefined' &&
+ Object.prototype.toString.call(window) !== '[object Object]';
- // UA sniffing for working around browser-specific quirks
- var UA = inBrowser && window.navigator.userAgent.toLowerCase();
- var isIE = UA && UA.indexOf('trident') > 0;
- var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
- var isAndroid = UA && UA.indexOf('android') > 0;
- var isIos = UA && /(iphone|ipad|ipod|ios)/i.test(UA);
- var iosVersionMatch = isIos && UA.match(/os ([\d_]+)/);
- var iosVersion = iosVersionMatch && iosVersionMatch[1].split('_');
+var UA = inBrowser && window.navigator.userAgent.toLowerCase();
+var isIE = UA && /msie|trident/.test(UA);
+var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
+var isEdge = UA && UA.indexOf('edge/') > 0;
+var isAndroid = UA && UA.indexOf('android') > 0;
+var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA);
- // detecting iOS UIWebView by indexedDB
- var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB;
+// detect devtools
+var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
- var transitionProp = undefined;
- var transitionEndEvent = undefined;
- var animationProp = undefined;
- var animationEndEvent = undefined;
+/* istanbul ignore next */
+function isNative (Ctor) {
+ return /native code/.test(Ctor.toString())
+}
- // Transition property/event sniffing
- if (inBrowser && !isIE9) {
- var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined;
- var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined;
- transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition';
- transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend';
- animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation';
- animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend';
+/**
+ * Defer a task to execute it asynchronously.
+ */
+var nextTick = (function () {
+ var callbacks = [];
+ var pending = false;
+ var timerFunc;
+
+ function nextTickHandler () {
+ pending = false;
+ var copies = callbacks.slice(0);
+ callbacks.length = 0;
+ for (var i = 0; i < copies.length; i++) {
+ copies[i]();
+ }
+ }
+
+ // the nextTick behavior leverages the microtask queue, which can be accessed
+ // via either native Promise.then or MutationObserver.
+ // MutationObserver has wider support, however it is seriously bugged in
+ // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
+ // completely stops working after triggering a few times... so, if native
+ // Promise is available, we will use it:
+ /* istanbul ignore if */
+ if (typeof Promise !== 'undefined' && isNative(Promise)) {
+ var p = Promise.resolve();
+ timerFunc = function () {
+ p.then(nextTickHandler);
+ // in problematic UIWebViews, Promise.then doesn't completely break, but
+ // it can get stuck in a weird state where callbacks are pushed into the
+ // microtask queue but the queue isn't being flushed, until the browser
+ // needs to do some other work, e.g. handle a timer. Therefore we can
+ // "force" the microtask queue to be flushed by adding an empty timer.
+ if (isIOS) { setTimeout(noop); }
+ };
+ } else if (typeof MutationObserver !== 'undefined' && (
+ isNative(MutationObserver) ||
+ // PhantomJS and iOS 7.x
+ MutationObserver.toString() === '[object MutationObserverConstructor]'
+ )) {
+ // use MutationObserver where native Promise is not available,
+ // e.g. PhantomJS IE11, iOS7, Android 4.4
+ var counter = 1;
+ var observer = new MutationObserver(nextTickHandler);
+ var textNode = document.createTextNode(String(counter));
+ observer.observe(textNode, {
+ characterData: true
+ });
+ timerFunc = function () {
+ counter = (counter + 1) % 2;
+ textNode.data = String(counter);
+ };
+ } else {
+ // fallback to setTimeout
+ /* istanbul ignore next */
+ timerFunc = function () {
+ setTimeout(nextTickHandler, 0);
+ };
}
- /**
- * Defer a task to execute it asynchronously. Ideally this
- * should be executed as a microtask, so we leverage
- * MutationObserver if it's available, and fallback to
- * setTimeout(0).
- *
- * @param {Function} cb
- * @param {Object} ctx
- */
-
- var nextTick = (function () {
- var callbacks = [];
- var pending = false;
- var timerFunc;
- function nextTickHandler() {
- pending = false;
- var copies = callbacks.slice(0);
- callbacks = [];
- for (var i = 0; i < copies.length; i++) {
- copies[i]();
- }
- }
-
- /* istanbul ignore if */
- if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) {
- var counter = 1;
- var observer = new MutationObserver(nextTickHandler);
- var textNode = document.createTextNode(counter);
- observer.observe(textNode, {
- characterData: true
- });
- timerFunc = function () {
- counter = (counter + 1) % 2;
- textNode.data = counter;
- };
- } else {
- // webpack attempts to inject a shim for setImmediate
- // if it is used as a global, so we have to work around that to
- // avoid bundling unnecessary code.
- var context = inBrowser ? window : typeof global !== 'undefined' ? global : {};
- timerFunc = context.setImmediate || setTimeout;
- }
- return function (cb, ctx) {
- var func = ctx ? function () {
- cb.call(ctx);
- } : cb;
- callbacks.push(func);
- if (pending) return;
+ return function queueNextTick (cb, ctx) {
+ var func = ctx
+ ? function () { cb.call(ctx); }
+ : cb;
+ callbacks.push(func);
+ if (!pending) {
pending = true;
- timerFunc(nextTickHandler, 0);
- };
- })();
+ timerFunc();
+ }
+ }
+})();
- var _Set = undefined;
- /* istanbul ignore if */
- if (typeof Set !== 'undefined' && Set.toString().match(/native code/)) {
- // use native Set when available.
- _Set = Set;
- } else {
- // a non-standard Set polyfill that only works with primitive keys.
- _Set = function () {
+var _Set;
+/* istanbul ignore if */
+if (typeof Set !== 'undefined' && isNative(Set)) {
+ // use native Set when available.
+ _Set = Set;
+} else {
+ // a non-standard Set polyfill that only works with primitive keys.
+ _Set = (function () {
+ function Set () {
this.set = Object.create(null);
+ }
+ Set.prototype.has = function has (key) {
+ return this.set[key] !== undefined
};
- _Set.prototype.has = function (key) {
- return this.set[key] !== undefined;
- };
- _Set.prototype.add = function (key) {
+ Set.prototype.add = function add (key) {
this.set[key] = 1;
};
- _Set.prototype.clear = function () {
+ Set.prototype.clear = function clear () {
this.set = Object.create(null);
};
- }
-
- function Cache(limit) {
- this.size = 0;
- this.limit = limit;
- this.head = this.tail = undefined;
- this._keymap = Object.create(null);
- }
-
- var p = Cache.prototype;
- /**
- * Put <value> into the cache associated with <key>.
- * Returns the entry which was removed to make room for
- * the new entry. Otherwise undefined is returned.
- * (i.e. if there was enough room already).
- *
- * @param {String} key
- * @param {*} value
- * @return {Entry|undefined}
- */
-
- p.put = function (key, value) {
- var removed;
-
- var entry = this.get(key, true);
- if (!entry) {
- if (this.size === this.limit) {
- removed = this.shift();
- }
- entry = {
- key: key
- };
- this._keymap[key] = entry;
- if (this.tail) {
- this.tail.newer = entry;
- entry.older = this.tail;
- } else {
- this.head = entry;
+ return Set;
+ }());
+}
+
+/* not type checking this file because flow doesn't play well with Proxy */
+
+var hasProxy;
+var proxyHandlers;
+var initProxy;
+
+{
+ var allowedGlobals = makeMap(
+ 'Infinity,undefined,NaN,isFinite,isNaN,' +
+ 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
+ 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
+ 'require' // for Webpack/Browserify
+ );
+
+ hasProxy =
+ typeof Proxy !== 'undefined' &&
+ Proxy.toString().match(/native code/);
+
+ proxyHandlers = {
+ has: function has (target, key) {
+ var has = key in target;
+ var isAllowed = allowedGlobals(key) || key.charAt(0) === '_';
+ if (!has && !isAllowed) {
+ warn(
+ "Property or method \"" + key + "\" is not defined on the instance but " +
+ "referenced during render. Make sure to declare reactive data " +
+ "properties in the data option.",
+ target
+ );
}
- this.tail = entry;
- this.size++;
+ return has || !isAllowed
}
- entry.value = value;
-
- return removed;
};
- /**
- * Purge the least recently used (oldest) entry from the
- * cache. Returns the removed entry or undefined if the
- * cache was empty.
- */
-
- p.shift = function () {
- var entry = this.head;
- if (entry) {
- this.head = this.head.newer;
- this.head.older = undefined;
- entry.newer = entry.older = undefined;
- this._keymap[entry.key] = undefined;
- this.size--;
+ initProxy = function initProxy (vm) {
+ if (hasProxy) {
+ vm._renderProxy = new Proxy(vm, proxyHandlers);
+ } else {
+ vm._renderProxy = vm;
}
- return entry;
};
+}
- /**
- * Get and register recent use of <key>. Returns the value
- * associated with <key> or undefined if not in cache.
- *
- * @param {String} key
- * @param {Boolean} returnEntry
- * @return {Entry|*}
- */
+/* */
- p.get = function (key, returnEntry) {
- var entry = this._keymap[key];
- if (entry === undefined) return;
- if (entry === this.tail) {
- return returnEntry ? entry : entry.value;
- }
- // HEAD--------------TAIL
- // <.older .newer>
- // <--- add direction --
- // A B C <D> E
- if (entry.newer) {
- if (entry === this.head) {
- this.head = entry.newer;
- }
- entry.newer.older = entry.older; // C <-- E.
- }
- if (entry.older) {
- entry.older.newer = entry.newer; // C. --> E
- }
- entry.newer = undefined; // D --x
- entry.older = this.tail; // D. --> E
- if (this.tail) {
- this.tail.newer = entry; // E. <-- D
- }
- this.tail = entry;
- return returnEntry ? entry : entry.value;
- };
- var cache$1 = new Cache(1000);
- var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g;
- var reservedArgRE = /^in$|^-?\d+/;
+var uid$2 = 0;
- /**
- * Parser state
- */
-
- var str;
- var dir;
- var c;
- var prev;
- var i;
- var l;
- var lastFilterIndex;
- var inSingle;
- var inDouble;
- var curly;
- var square;
- var paren;
- /**
- * Push a filter to the current directive object
- */
+/**
+ * A dep is an observable that can have multiple
+ * directives subscribing to it.
+ */
+var Dep = function Dep () {
+ this.id = uid$2++;
+ this.subs = [];
+};
- function pushFilter() {
- var exp = str.slice(lastFilterIndex, i).trim();
- var filter;
- if (exp) {
- filter = {};
- var tokens = exp.match(filterTokenRE);
- filter.name = tokens[0];
- if (tokens.length > 1) {
- filter.args = tokens.slice(1).map(processFilterArg);
- }
- }
- if (filter) {
- (dir.filters = dir.filters || []).push(filter);
- }
- lastFilterIndex = i + 1;
- }
+Dep.prototype.addSub = function addSub (sub) {
+ this.subs.push(sub);
+};
- /**
- * Check if an argument is dynamic and strip quotes.
- *
- * @param {String} arg
- * @return {Object}
- */
+Dep.prototype.removeSub = function removeSub (sub) {
+ remove$1(this.subs, sub);
+};
- function processFilterArg(arg) {
- if (reservedArgRE.test(arg)) {
- return {
- value: toNumber(arg),
- dynamic: false
- };
- } else {
- var stripped = stripQuotes(arg);
- var dynamic = stripped === arg;
- return {
- value: dynamic ? arg : stripped,
- dynamic: dynamic
- };
- }
+Dep.prototype.depend = function depend () {
+ if (Dep.target) {
+ Dep.target.addDep(this);
}
+};
- /**
- * Parse a directive value and extract the expression
- * and its filters into a descriptor.
- *
- * Example:
- *
- * "a + 1 | uppercase" will yield:
- * {
- * expression: 'a + 1',
- * filters: [
- * { name: 'uppercase', args: null }
- * ]
- * }
- *
- * @param {String} s
- * @return {Object}
- */
+Dep.prototype.notify = function notify () {
+ // stablize the subscriber list first
+ var subs = this.subs.slice();
+ for (var i = 0, l = subs.length; i < l; i++) {
+ subs[i].update();
+ }
+};
- function parseDirective(s) {
- var hit = cache$1.get(s);
- if (hit) {
- return hit;
- }
-
- // reset parser state
- str = s;
- inSingle = inDouble = false;
- curly = square = paren = 0;
- lastFilterIndex = 0;
- dir = {};
-
- for (i = 0, l = str.length; i < l; i++) {
- prev = c;
- c = str.charCodeAt(i);
- if (inSingle) {
- // check single quote
- if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle;
- } else if (inDouble) {
- // check double quote
- if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble;
- } else if (c === 0x7C && // pipe
- str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) {
- if (dir.expression == null) {
- // first filter, end of expression
- lastFilterIndex = i + 1;
- dir.expression = str.slice(0, i).trim();
- } else {
- // already has filter
- pushFilter();
- }
- } else {
- switch (c) {
- case 0x22:
- inDouble = true;break; // "
- case 0x27:
- inSingle = true;break; // '
- case 0x28:
- paren++;break; // (
- case 0x29:
- paren--;break; // )
- case 0x5B:
- square++;break; // [
- case 0x5D:
- square--;break; // ]
- case 0x7B:
- curly++;break; // {
- case 0x7D:
- curly--;break; // }
- }
- }
- }
+// the current target watcher being evaluated.
+// this is globally unique because there could be only one
+// watcher being evaluated at any time.
+Dep.target = null;
+var targetStack = [];
- if (dir.expression == null) {
- dir.expression = str.slice(0, i).trim();
- } else if (lastFilterIndex !== 0) {
- pushFilter();
- }
+function pushTarget (_target) {
+ if (Dep.target) { targetStack.push(Dep.target); }
+ Dep.target = _target;
+}
- cache$1.put(s, dir);
- return dir;
- }
+function popTarget () {
+ Dep.target = targetStack.pop();
+}
-var directive = Object.freeze({
- parseDirective: parseDirective
- });
+/* */
- var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
- var cache = undefined;
- var tagRE = undefined;
- var htmlRE = undefined;
- /**
- * Escape a string so it can be used in a RegExp
- * constructor.
- *
- * @param {String} str
- */
- function escapeRegex(str) {
- return str.replace(regexEscapeRE, '\\$&');
- }
+var queue = [];
+var has$1 = {};
+var circular = {};
+var waiting = false;
+var flushing = false;
+var index = 0;
- function compileRegex() {
- var open = escapeRegex(config.delimiters[0]);
- var close = escapeRegex(config.delimiters[1]);
- var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]);
- var unsafeClose = escapeRegex(config.unsafeDelimiters[1]);
- tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g');
- htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$');
- // reset cache
- cache = new Cache(1000);
+/**
+ * Reset the scheduler's state.
+ */
+function resetSchedulerState () {
+ queue.length = 0;
+ has$1 = {};
+ {
+ circular = {};
}
+ waiting = flushing = false;
+}
- /**
- * Parse a template text string into an array of tokens.
- *
- * @param {String} text
- * @return {Array<Object> | null}
- * - {String} type
- * - {String} value
- * - {Boolean} [html]
- * - {Boolean} [oneTime]
- */
-
- function parseText(text) {
- if (!cache) {
- compileRegex();
- }
- var hit = cache.get(text);
- if (hit) {
- return hit;
- }
- if (!tagRE.test(text)) {
- return null;
- }
- var tokens = [];
- var lastIndex = tagRE.lastIndex = 0;
- var match, index, html, value, first, oneTime;
- /* eslint-disable no-cond-assign */
- while (match = tagRE.exec(text)) {
- /* eslint-enable no-cond-assign */
- index = match.index;
- // push text token
- if (index > lastIndex) {
- tokens.push({
- value: text.slice(lastIndex, index)
- });
+/**
+ * Flush both queues and run the watchers.
+ */
+function flushSchedulerQueue () {
+ flushing = true;
+
+ // Sort queue before flush.
+ // This ensures that:
+ // 1. Components are updated from parent to child. (because parent is always
+ // created before the child)
+ // 2. A component's user watchers are run before its render watcher (because
+ // user watchers are created before the render watcher)
+ // 3. If a component is destroyed during a parent component's watcher run,
+ // its watchers can be skipped.
+ queue.sort(function (a, b) { return a.id - b.id; });
+
+ // do not cache length because more watchers might be pushed
+ // as we run existing watchers
+ for (index = 0; index < queue.length; index++) {
+ var watcher = queue[index];
+ var id = watcher.id;
+ has$1[id] = null;
+ watcher.run();
+ // in dev build, check and stop circular updates.
+ if ("development" !== 'production' && has$1[id] != null) {
+ circular[id] = (circular[id] || 0) + 1;
+ if (circular[id] > config._maxUpdateCount) {
+ warn(
+ 'You may have an infinite update loop ' + (
+ watcher.user
+ ? ("in watcher with expression \"" + (watcher.expression) + "\"")
+ : "in a component render function."
+ ),
+ watcher.vm
+ );
+ break
}
- // tag token
- html = htmlRE.test(match[0]);
- value = html ? match[1] : match[2];
- first = value.charCodeAt(0);
- oneTime = first === 42; // *
- value = oneTime ? value.slice(1) : value;
- tokens.push({
- tag: true,
- value: value.trim(),
- html: html,
- oneTime: oneTime
- });
- lastIndex = index + match[0].length;
- }
- if (lastIndex < text.length) {
- tokens.push({
- value: text.slice(lastIndex)
- });
- }
- cache.put(text, tokens);
- return tokens;
- }
-
- /**
- * Format a list of tokens into an expression.
- * e.g. tokens parsed from 'a {{b}} c' can be serialized
- * into one single expression as '"a " + b + " c"'.
- *
- * @param {Array} tokens
- * @param {Vue} [vm]
- * @return {String}
- */
-
- function tokensToExp(tokens, vm) {
- if (tokens.length > 1) {
- return tokens.map(function (token) {
- return formatToken(token, vm);
- }).join('+');
- } else {
- return formatToken(tokens[0], vm, true);
}
}
- /**
- * Format a single token.
- *
- * @param {Object} token
- * @param {Vue} [vm]
- * @param {Boolean} [single]
- * @return {String}
- */
-
- function formatToken(token, vm, single) {
- return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"';
+ // devtool hook
+ /* istanbul ignore if */
+ if (devtools && config.devtools) {
+ devtools.emit('flush');
}
- /**
- * For an attribute with multiple interpolation tags,
- * e.g. attr="some-{{thing | filter}}", in order to combine
- * the whole thing into a single watchable expression, we
- * have to inline those filters. This function does exactly
- * that. This is a bit hacky but it avoids heavy changes
- * to directive parser and watcher mechanism.
- *
- * @param {String} exp
- * @param {Boolean} single
- * @return {String}
- */
+ resetSchedulerState();
+}
- var filterRE = /[^|]\|[^|]/;
- function inlineFilters(exp, single) {
- if (!filterRE.test(exp)) {
- return single ? exp : '(' + exp + ')';
+/**
+ * Push a watcher into the watcher queue.
+ * Jobs with duplicate IDs will be skipped unless it's
+ * pushed when the queue is being flushed.
+ */
+function queueWatcher (watcher) {
+ var id = watcher.id;
+ if (has$1[id] == null) {
+ has$1[id] = true;
+ if (!flushing) {
+ queue.push(watcher);
} else {
- var dir = parseDirective(exp);
- if (!dir.filters) {
- return '(' + exp + ')';
- } else {
- return 'this._applyFilters(' + dir.expression + // value
- ',null,' + // oldValue (null for read)
- JSON.stringify(dir.filters) + // filter descriptors
- ',false)'; // write?
+ // if already flushing, splice the watcher based on its id
+ // if already past its id, it will be run next immediately.
+ var i = queue.length - 1;
+ while (i >= 0 && queue[i].id > watcher.id) {
+ i--;
}
+ queue.splice(Math.max(i, index) + 1, 0, watcher);
}
- }
-
-var text = Object.freeze({
- compileRegex: compileRegex,
- parseText: parseText,
- tokensToExp: tokensToExp
- });
-
- var delimiters = ['{{', '}}'];
- var unsafeDelimiters = ['{{{', '}}}'];
-
- var config = Object.defineProperties({
-
- /**
- * Whether to print debug messages.
- * Also enables stack trace for warnings.
- *
- * @type {Boolean}
- */
-
- debug: false,
-
- /**
- * Whether to suppress warnings.
- *
- * @type {Boolean}
- */
-
- silent: false,
-
- /**
- * Whether to use async rendering.
- */
-
- async: true,
-
- /**
- * Whether to warn against errors caught when evaluating
- * expressions.
- */
-
- warnExpressionErrors: true,
-
- /**
- * Whether to allow devtools inspection.
- * Disabled by default in production builds.
- */
-
- devtools: 'development' !== 'production',
-
- /**
- * Internal flag to indicate the delimiters have been
- * changed.
- *
- * @type {Boolean}
- */
-
- _delimitersChanged: true,
-
- /**
- * List of asset types that a component can own.
- *
- * @type {Array}
- */
-
- _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'],
-
- /**
- * prop binding modes
- */
-
- _propBindingModes: {
- ONE_WAY: 0,
- TWO_WAY: 1,
- ONE_TIME: 2
- },
-
- /**
- * Max circular updates allowed in a batcher flush cycle.
- */
-
- _maxUpdateCount: 100
-
- }, {
- delimiters: { /**
- * Interpolation delimiters. Changing these would trigger
- * the text parser to re-compile the regular expressions.
- *
- * @type {Array<String>}
- */
-
- get: function get() {
- return delimiters;
- },
- set: function set(val) {
- delimiters = val;
- compileRegex();
- },
- configurable: true,
- enumerable: true
- },
- unsafeDelimiters: {
- get: function get() {
- return unsafeDelimiters;
- },
- set: function set(val) {
- unsafeDelimiters = val;
- compileRegex();
- },
- configurable: true,
- enumerable: true
+ // queue the flush
+ if (!waiting) {
+ waiting = true;
+ nextTick(flushSchedulerQueue);
}
- });
-
- var warn = undefined;
- var formatComponentName = undefined;
-
- if ('development' !== 'production') {
- (function () {
- var hasConsole = typeof console !== 'undefined';
-
- warn = function (msg, vm) {
- if (hasConsole && !config.silent) {
- console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''));
- }
- };
-
- formatComponentName = function (vm) {
- var name = vm._isVue ? vm.$options.name : vm.name;
- return name ? ' (found in component: <' + hyphenate(name) + '>)' : '';
- };
- })();
}
+}
- /**
- * Append with transition.
- *
- * @param {Element} el
- * @param {Element} target
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function appendWithTransition(el, target, vm, cb) {
- applyTransition(el, 1, function () {
- target.appendChild(el);
- }, vm, cb);
- }
+/* */
- /**
- * InsertBefore with transition.
- *
- * @param {Element} el
- * @param {Element} target
- * @param {Vue} vm
- * @param {Function} [cb]
- */
+var uid$1 = 0;
- function beforeWithTransition(el, target, vm, cb) {
- applyTransition(el, 1, function () {
- before(el, target);
- }, vm, cb);
+/**
+ * A watcher parses an expression, collects dependencies,
+ * and fires callback when the expression value changes.
+ * This is used for both the $watch() api and directives.
+ */
+var Watcher = function Watcher (
+ vm,
+ expOrFn,
+ cb,
+ options
+) {
+ if ( options === void 0 ) options = {};
+
+ this.vm = vm;
+ vm._watchers.push(this);
+ // options
+ this.deep = !!options.deep;
+ this.user = !!options.user;
+ this.lazy = !!options.lazy;
+ this.sync = !!options.sync;
+ this.expression = expOrFn.toString();
+ this.cb = cb;
+ this.id = ++uid$1; // uid for batching
+ this.active = true;
+ this.dirty = this.lazy; // for lazy watchers
+ this.deps = [];
+ this.newDeps = [];
+ this.depIds = new _Set();
+ this.newDepIds = new _Set();
+ // parse expression for getter
+ if (typeof expOrFn === 'function') {
+ this.getter = expOrFn;
+ } else {
+ this.getter = parsePath(expOrFn);
+ if (!this.getter) {
+ this.getter = function () {};
+ "development" !== 'production' && warn(
+ "Failed watching path: \"" + expOrFn + "\" " +
+ 'Watcher only accepts simple dot-delimited paths. ' +
+ 'For full control, use a function instead.',
+ vm
+ );
+ }
+ }
+ this.value = this.lazy
+ ? undefined
+ : this.get();
+};
+
+/**
+ * Evaluate the getter, and re-collect dependencies.
+ */
+Watcher.prototype.get = function get () {
+ pushTarget(this);
+ var value = this.getter.call(this.vm, this.vm);
+ // "touch" every property so they are all tracked as
+ // dependencies for deep watching
+ if (this.deep) {
+ traverse(value);
+ }
+ popTarget();
+ this.cleanupDeps();
+ return value
+};
+
+/**
+ * Add a dependency to this directive.
+ */
+Watcher.prototype.addDep = function addDep (dep) {
+ var id = dep.id;
+ if (!this.newDepIds.has(id)) {
+ this.newDepIds.add(id);
+ this.newDeps.push(dep);
+ if (!this.depIds.has(id)) {
+ dep.addSub(this);
+ }
}
+};
- /**
- * Remove with transition.
- *
- * @param {Element} el
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function removeWithTransition(el, vm, cb) {
- applyTransition(el, -1, function () {
- remove(el);
- }, vm, cb);
+/**
+ * Clean up for dependency collection.
+ */
+Watcher.prototype.cleanupDeps = function cleanupDeps () {
+ var this$1 = this;
+
+ var i = this.deps.length;
+ while (i--) {
+ var dep = this$1.deps[i];
+ if (!this$1.newDepIds.has(dep.id)) {
+ dep.removeSub(this$1);
+ }
+ }
+ var tmp = this.depIds;
+ this.depIds = this.newDepIds;
+ this.newDepIds = tmp;
+ this.newDepIds.clear();
+ tmp = this.deps;
+ this.deps = this.newDeps;
+ this.newDeps = tmp;
+ this.newDeps.length = 0;
+};
+
+/**
+ * Subscriber interface.
+ * Will be called when a dependency changes.
+ */
+Watcher.prototype.update = function update () {
+ /* istanbul ignore else */
+ if (this.lazy) {
+ this.dirty = true;
+ } else if (this.sync) {
+ this.run();
+ } else {
+ queueWatcher(this);
}
+};
- /**
- * Apply transitions with an operation callback.
- *
- * @param {Element} el
- * @param {Number} direction
- * 1: enter
- * -1: leave
- * @param {Function} op - the actual DOM operation
- * @param {Vue} vm
- * @param {Function} [cb]
- */
-
- function applyTransition(el, direction, op, vm, cb) {
- var transition = el.__v_trans;
- if (!transition ||
- // skip if there are no js hooks and CSS transition is
- // not supported
- !transition.hooks && !transitionEndEvent ||
- // skip transitions for initial compile
- !vm._isCompiled ||
- // if the vm is being manipulated by a parent directive
- // during the parent's compilation phase, skip the
- // animation.
- vm.$parent && !vm.$parent._isCompiled) {
- op();
- if (cb) cb();
- return;
- }
- var action = direction > 0 ? 'enter' : 'leave';
- transition[action](op, cb);
- }
-
-var transition = Object.freeze({
- appendWithTransition: appendWithTransition,
- beforeWithTransition: beforeWithTransition,
- removeWithTransition: removeWithTransition,
- applyTransition: applyTransition
- });
-
- /**
- * Query an element selector if it's not an element already.
- *
- * @param {String|Element} el
- * @return {Element}
- */
-
- function query(el) {
- if (typeof el === 'string') {
- var selector = el;
- el = document.querySelector(el);
- if (!el) {
- 'development' !== 'production' && warn('Cannot find element: ' + selector);
+/**
+ * Scheduler job interface.
+ * Will be called by the scheduler.
+ */
+Watcher.prototype.run = function run () {
+ if (this.active) {
+ var value = this.get();
+ if (
+ value !== this.value ||
+ // Deep watchers and watchers on Object/Arrays should fire even
+ // when the value is the same, because the value may
+ // have mutated.
+ isObject(value) ||
+ this.deep
+ ) {
+ // set new value
+ var oldValue = this.value;
+ this.value = value;
+ if (this.user) {
+ try {
+ this.cb.call(this.vm, value, oldValue);
+ } catch (e) {
+ "development" !== 'production' && warn(
+ ("Error in watcher \"" + (this.expression) + "\""),
+ this.vm
+ );
+ /* istanbul ignore else */
+ if (config.errorHandler) {
+ config.errorHandler.call(null, e, this.vm);
+ } else {
+ throw e
+ }
+ }
+ } else {
+ this.cb.call(this.vm, value, oldValue);
}
}
- return el;
}
+};
- /**
- * Check if a node is in the document.
- * Note: document.documentElement.contains should work here
- * but always returns false for comment nodes in phantomjs,
- * making unit tests difficult. This is fixed by doing the
- * contains() check on the node's parentNode instead of
- * the node itself.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function inDoc(node) {
- if (!node) return false;
- var doc = node.ownerDocument.documentElement;
- var parent = node.parentNode;
- return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent));
- }
+/**
+ * Evaluate the value of the watcher.
+ * This only gets called for lazy watchers.
+ */
+Watcher.prototype.evaluate = function evaluate () {
+ this.value = this.get();
+ this.dirty = false;
+};
- /**
- * Get and remove an attribute from a node.
- *
- * @param {Node} node
- * @param {String} _attr
- */
+/**
+ * Depend on all deps collected by this watcher.
+ */
+Watcher.prototype.depend = function depend () {
+ var this$1 = this;
- function getAttr(node, _attr) {
- var val = node.getAttribute(_attr);
- if (val !== null) {
- node.removeAttribute(_attr);
- }
- return val;
+ var i = this.deps.length;
+ while (i--) {
+ this$1.deps[i].depend();
}
+};
- /**
- * Get an attribute with colon or v-bind: prefix.
- *
- * @param {Node} node
- * @param {String} name
- * @return {String|null}
- */
+/**
+ * Remove self from all dependencies' subcriber list.
+ */
+Watcher.prototype.teardown = function teardown () {
+ var this$1 = this;
- function getBindAttr(node, name) {
- var val = getAttr(node, ':' + name);
- if (val === null) {
- val = getAttr(node, 'v-bind:' + name);
+ if (this.active) {
+ // remove self from vm's watcher list
+ // this is a somewhat expensive operation so we skip it
+ // if the vm is being destroyed or is performing a v-for
+ // re-render (the watcher list is then filtered by v-for).
+ if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
+ remove$1(this.vm._watchers, this);
}
- return val;
- }
-
- /**
- * Check the presence of a bind attribute.
- *
- * @param {Node} node
- * @param {String} name
- * @return {Boolean}
- */
-
- function hasBindAttr(node, name) {
- return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name);
- }
-
- /**
- * Insert el before target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function before(el, target) {
- target.parentNode.insertBefore(el, target);
- }
-
- /**
- * Insert el after target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function after(el, target) {
- if (target.nextSibling) {
- before(el, target.nextSibling);
- } else {
- target.parentNode.appendChild(el);
+ var i = this.deps.length;
+ while (i--) {
+ this$1.deps[i].removeSub(this$1);
}
+ this.active = false;
}
+};
- /**
- * Remove el from DOM
- *
- * @param {Element} el
- */
-
- function remove(el) {
- el.parentNode.removeChild(el);
- }
-
- /**
- * Prepend el to target
- *
- * @param {Element} el
- * @param {Element} target
- */
-
- function prepend(el, target) {
- if (target.firstChild) {
- before(el, target.firstChild);
- } else {
- target.appendChild(el);
+/**
+ * Recursively traverse an object to evoke all converted
+ * getters, so that every nested property inside the object
+ * is collected as a "deep" dependency.
+ */
+var seenObjects = new _Set();
+function traverse (val, seen) {
+ var i, keys;
+ if (!seen) {
+ seen = seenObjects;
+ seen.clear();
+ }
+ var isA = Array.isArray(val);
+ var isO = isObject(val);
+ if ((isA || isO) && Object.isExtensible(val)) {
+ if (val.__ob__) {
+ var depId = val.__ob__.dep.id;
+ if (seen.has(depId)) {
+ return
+ } else {
+ seen.add(depId);
+ }
}
- }
-
- /**
- * Replace target with el
- *
- * @param {Element} target
- * @param {Element} el
- */
-
- function replace(target, el) {
- var parent = target.parentNode;
- if (parent) {
- parent.replaceChild(el, target);
+ if (isA) {
+ i = val.length;
+ while (i--) { traverse(val[i], seen); }
+ } else if (isO) {
+ keys = Object.keys(val);
+ i = keys.length;
+ while (i--) { traverse(val[keys[i]], seen); }
}
}
+}
- /**
- * Add event listener shorthand.
- *
- * @param {Element} el
- * @param {String} event
- * @param {Function} cb
- * @param {Boolean} [useCapture]
- */
+/*
+ * not type checking this file because flow doesn't play well with
+ * dynamically accessing methods on Array prototype
+ */
- function on(el, event, cb, useCapture) {
- el.addEventListener(event, cb, useCapture);
- }
+var arrayProto = Array.prototype;
+var arrayMethods = Object.create(arrayProto);[
+ 'push',
+ 'pop',
+ 'shift',
+ 'unshift',
+ 'splice',
+ 'sort',
+ 'reverse'
+]
+.forEach(function (method) {
+ // cache original method
+ var original = arrayProto[method];
+ def(arrayMethods, method, function mutator () {
+ var arguments$1 = arguments;
+
+ // avoid leaking arguments:
+ // http://jsperf.com/closure-with-arguments
+ var i = arguments.length;
+ var args = new Array(i);
+ while (i--) {
+ args[i] = arguments$1[i];
+ }
+ var result = original.apply(this, args);
+ var ob = this.__ob__;
+ var inserted;
+ switch (method) {
+ case 'push':
+ inserted = args;
+ break
+ case 'unshift':
+ inserted = args;
+ break
+ case 'splice':
+ inserted = args.slice(2);
+ break
+ }
+ if (inserted) { ob.observeArray(inserted); }
+ // notify change
+ ob.dep.notify();
+ return result
+ });
+});
- /**
- * Remove event listener shorthand.
- *
- * @param {Element} el
- * @param {String} event
- * @param {Function} cb
- */
+/* */
+
+var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
- function off(el, event, cb) {
- el.removeEventListener(event, cb);
+/**
+ * By default, when a reactive property is set, the new value is
+ * also converted to become reactive. However when passing down props,
+ * we don't want to force conversion because the value may be a nested value
+ * under a frozen data structure. Converting it would defeat the optimization.
+ */
+var observerState = {
+ shouldConvert: true,
+ isSettingProps: false
+};
+
+/**
+ * Observer class that are attached to each observed
+ * object. Once attached, the observer converts target
+ * object's property keys into getter/setters that
+ * collect dependencies and dispatches updates.
+ */
+var Observer = function Observer (value) {
+ this.value = value;
+ this.dep = new Dep();
+ this.vmCount = 0;
+ def(value, '__ob__', this);
+ if (Array.isArray(value)) {
+ var augment = hasProto
+ ? protoAugment
+ : copyAugment;
+ augment(value, arrayMethods, arrayKeys);
+ this.observeArray(value);
+ } else {
+ this.walk(value);
}
+};
- /**
- * For IE9 compat: when both class and :class are present
- * getAttribute('class') returns wrong value...
- *
- * @param {Element} el
- * @return {String}
- */
+/**
+ * Walk through each property and convert them into
+ * getter/setters. This method should only be called when
+ * value type is Object.
+ */
+Observer.prototype.walk = function walk (obj) {
+ var keys = Object.keys(obj);
+ for (var i = 0; i < keys.length; i++) {
+ defineReactive$$1(obj, keys[i], obj[keys[i]]);
+ }
+};
- function getClass(el) {
- var classname = el.className;
- if (typeof classname === 'object') {
- classname = classname.baseVal || '';
- }
- return classname;
+/**
+ * Observe a list of Array items.
+ */
+Observer.prototype.observeArray = function observeArray (items) {
+ for (var i = 0, l = items.length; i < l; i++) {
+ observe(items[i]);
}
+};
- /**
- * In IE9, setAttribute('class') will result in empty class
- * if the element also has the :class attribute; However in
- * PhantomJS, setting `className` does not work on SVG elements...
- * So we have to do a conditional check here.
- *
- * @param {Element} el
- * @param {String} cls
- */
+// helpers
- function setClass(el, cls) {
- /* istanbul ignore if */
- if (isIE9 && !/svg$/.test(el.namespaceURI)) {
- el.className = cls;
- } else {
- el.setAttribute('class', cls);
- }
+/**
+ * Augment an target Object or Array by intercepting
+ * the prototype chain using __proto__
+ */
+function protoAugment (target, src) {
+ /* eslint-disable no-proto */
+ target.__proto__ = src;
+ /* eslint-enable no-proto */
+}
+
+/**
+ * Augment an target Object or Array by defining
+ * hidden properties.
+ *
+ * istanbul ignore next
+ */
+function copyAugment (target, src, keys) {
+ for (var i = 0, l = keys.length; i < l; i++) {
+ var key = keys[i];
+ def(target, key, src[key]);
}
+}
- /**
- * Add class with compatibility for IE & SVG
- *
- * @param {Element} el
- * @param {String} cls
- */
-
- function addClass(el, cls) {
- if (el.classList) {
- el.classList.add(cls);
- } else {
- var cur = ' ' + getClass(el) + ' ';
- if (cur.indexOf(' ' + cls + ' ') < 0) {
- setClass(el, (cur + cls).trim());
+/**
+ * Attempt to create an observer instance for a value,
+ * returns the new observer if successfully observed,
+ * or the existing observer if the value already has one.
+ */
+function observe (value) {
+ if (!isObject(value)) {
+ return
+ }
+ var ob;
+ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+ ob = value.__ob__;
+ } else if (
+ observerState.shouldConvert &&
+ !config._isServer &&
+ (Array.isArray(value) || isPlainObject(value)) &&
+ Object.isExtensible(value) &&
+ !value._isVue
+ ) {
+ ob = new Observer(value);
+ }
+ return ob
+}
+
+/**
+ * Define a reactive property on an Object.
+ */
+function defineReactive$$1 (
+ obj,
+ key,
+ val,
+ customSetter
+) {
+ var dep = new Dep();
+
+ var property = Object.getOwnPropertyDescriptor(obj, key);
+ if (property && property.configurable === false) {
+ return
+ }
+
+ // cater for pre-defined getter/setters
+ var getter = property && property.get;
+ var setter = property && property.set;
+
+ var childOb = observe(val);
+ Object.defineProperty(obj, key, {
+ enumerable: true,
+ configurable: true,
+ get: function reactiveGetter () {
+ var value = getter ? getter.call(obj) : val;
+ if (Dep.target) {
+ dep.depend();
+ if (childOb) {
+ childOb.dep.depend();
+ }
+ if (Array.isArray(value)) {
+ dependArray(value);
+ }
+ }
+ return value
+ },
+ set: function reactiveSetter (newVal) {
+ var value = getter ? getter.call(obj) : val;
+ if (newVal === value) {
+ return
+ }
+ if ("development" !== 'production' && customSetter) {
+ customSetter();
+ }
+ if (setter) {
+ setter.call(obj, newVal);
+ } else {
+ val = newVal;
}
+ childOb = observe(newVal);
+ dep.notify();
}
- }
+ });
+}
- /**
- * Remove class with compatibility for IE & SVG
- *
- * @param {Element} el
- * @param {String} cls
- */
+/**
+ * Set a property on an object. Adds the new property and
+ * triggers change notification if the property doesn't
+ * already exist.
+ */
+function set (obj, key, val) {
+ if (Array.isArray(obj)) {
+ obj.splice(key, 1, val);
+ return val
+ }
+ if (hasOwn(obj, key)) {
+ obj[key] = val;
+ return
+ }
+ var ob = obj.__ob__;
+ if (obj._isVue || (ob && ob.vmCount)) {
+ "development" !== 'production' && warn(
+ 'Avoid adding reactive properties to a Vue instance or its root $data ' +
+ 'at runtime - declare it upfront in the data option.'
+ );
+ return
+ }
+ if (!ob) {
+ obj[key] = val;
+ return
+ }
+ defineReactive$$1(ob.value, key, val);
+ ob.dep.notify();
+ return val
+}
+
+/**
+ * Delete a property and trigger change if necessary.
+ */
+function del (obj, key) {
+ var ob = obj.__ob__;
+ if (obj._isVue || (ob && ob.vmCount)) {
+ "development" !== 'production' && warn(
+ 'Avoid deleting properties on a Vue instance or its root $data ' +
+ '- just set it to null.'
+ );
+ return
+ }
+ if (!hasOwn(obj, key)) {
+ return
+ }
+ delete obj[key];
+ if (!ob) {
+ return
+ }
+ ob.dep.notify();
+}
+
+/**
+ * Collect dependencies on array elements when the array is touched, since
+ * we cannot intercept array element access like property getters.
+ */
+function dependArray (value) {
+ for (var e = void 0, i = 0, l = value.length; i < l; i++) {
+ e = value[i];
+ e && e.__ob__ && e.__ob__.dep.depend();
+ if (Array.isArray(e)) {
+ dependArray(e);
+ }
+ }
+}
+
+/* */
+
+function initState (vm) {
+ vm._watchers = [];
+ initProps(vm);
+ initData(vm);
+ initComputed(vm);
+ initMethods(vm);
+ initWatch(vm);
+}
+
+function initProps (vm) {
+ var props = vm.$options.props;
+ if (props) {
+ var propsData = vm.$options.propsData || {};
+ var keys = vm.$options._propKeys = Object.keys(props);
+ var isRoot = !vm.$parent;
+ // root instance props should be converted
+ observerState.shouldConvert = isRoot;
+ var loop = function ( i ) {
+ var key = keys[i];
+ /* istanbul ignore else */
+ {
+ defineReactive$$1(vm, key, validateProp(key, props, propsData, vm), function () {
+ if (vm.$parent && !observerState.isSettingProps) {
+ warn(
+ "Avoid mutating a prop directly since the value will be " +
+ "overwritten whenever the parent component re-renders. " +
+ "Instead, use a data or computed property based on the prop's " +
+ "value. Prop being mutated: \"" + key + "\"",
+ vm
+ );
+ }
+ });
+ }
+ };
- function removeClass(el, cls) {
- if (el.classList) {
- el.classList.remove(cls);
+ for (var i = 0; i < keys.length; i++) loop( i );
+ observerState.shouldConvert = true;
+ }
+}
+
+function initData (vm) {
+ var data = vm.$options.data;
+ data = vm._data = typeof data === 'function'
+ ? data.call(vm)
+ : data || {};
+ if (!isPlainObject(data)) {
+ data = {};
+ "development" !== 'production' && warn(
+ 'data functions should return an object.',
+ vm
+ );
+ }
+ // proxy data on instance
+ var keys = Object.keys(data);
+ var props = vm.$options.props;
+ var i = keys.length;
+ while (i--) {
+ if (props && hasOwn(props, keys[i])) {
+ "development" !== 'production' && warn(
+ "The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
+ "Use prop default value instead.",
+ vm
+ );
} else {
- var cur = ' ' + getClass(el) + ' ';
- var tar = ' ' + cls + ' ';
- while (cur.indexOf(tar) >= 0) {
- cur = cur.replace(tar, ' ');
+ proxy(vm, keys[i]);
+ }
+ }
+ // observe data
+ observe(data);
+ data.__ob__ && data.__ob__.vmCount++;
+}
+
+var computedSharedDefinition = {
+ enumerable: true,
+ configurable: true,
+ get: noop,
+ set: noop
+};
+
+function initComputed (vm) {
+ var computed = vm.$options.computed;
+ if (computed) {
+ for (var key in computed) {
+ var userDef = computed[key];
+ if (typeof userDef === 'function') {
+ computedSharedDefinition.get = makeComputedGetter(userDef, vm);
+ computedSharedDefinition.set = noop;
+ } else {
+ computedSharedDefinition.get = userDef.get
+ ? userDef.cache !== false
+ ? makeComputedGetter(userDef.get, vm)
+ : bind$1(userDef.get, vm)
+ : noop;
+ computedSharedDefinition.set = userDef.set
+ ? bind$1(userDef.set, vm)
+ : noop;
}
- setClass(el, cur.trim());
- }
- if (!el.className) {
- el.removeAttribute('class');
+ Object.defineProperty(vm, key, computedSharedDefinition);
}
}
+}
- /**
- * Extract raw content inside an element into a temporary
- * container div
- *
- * @param {Element} el
- * @param {Boolean} asFragment
- * @return {Element|DocumentFragment}
- */
-
- function extractContent(el, asFragment) {
- var child;
- var rawContent;
- /* istanbul ignore if */
- if (isTemplate(el) && isFragment(el.content)) {
- el = el.content;
- }
- if (el.hasChildNodes()) {
- trimNode(el);
- rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div');
- /* eslint-disable no-cond-assign */
- while (child = el.firstChild) {
- /* eslint-enable no-cond-assign */
- rawContent.appendChild(child);
+function makeComputedGetter (getter, owner) {
+ var watcher = new Watcher(owner, getter, noop, {
+ lazy: true
+ });
+ return function computedGetter () {
+ if (watcher.dirty) {
+ watcher.evaluate();
+ }
+ if (Dep.target) {
+ watcher.depend();
+ }
+ return watcher.value
+ }
+}
+
+function initMethods (vm) {
+ var methods = vm.$options.methods;
+ if (methods) {
+ for (var key in methods) {
+ vm[key] = methods[key] == null ? noop : bind$1(methods[key], vm);
+ if ("development" !== 'production' && methods[key] == null) {
+ warn(
+ "method \"" + key + "\" has an undefined value in the component definition. " +
+ "Did you reference the function correctly?",
+ vm
+ );
}
}
- return rawContent;
}
+}
- /**
- * Trim possible empty head/tail text and comment
- * nodes inside a parent.
- *
- * @param {Node} node
- */
-
- function trimNode(node) {
- var child;
- /* eslint-disable no-sequences */
- while ((child = node.firstChild, isTrimmable(child))) {
- node.removeChild(child);
- }
- while ((child = node.lastChild, isTrimmable(child))) {
- node.removeChild(child);
+function initWatch (vm) {
+ var watch = vm.$options.watch;
+ if (watch) {
+ for (var key in watch) {
+ var handler = watch[key];
+ if (Array.isArray(handler)) {
+ for (var i = 0; i < handler.length; i++) {
+ createWatcher(vm, key, handler[i]);
+ }
+ } else {
+ createWatcher(vm, key, handler);
+ }
}
- /* eslint-enable no-sequences */
}
+}
- function isTrimmable(node) {
- return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8);
+function createWatcher (vm, key, handler) {
+ var options;
+ if (isPlainObject(handler)) {
+ options = handler;
+ handler = handler.handler;
}
-
- /**
- * Check if an element is a template tag.
- * Note if the template appears inside an SVG its tagName
- * will be in lowercase.
- *
- * @param {Element} el
- */
-
- function isTemplate(el) {
- return el.tagName && el.tagName.toLowerCase() === 'template';
+ if (typeof handler === 'string') {
+ handler = vm[handler];
}
+ vm.$watch(key, handler, options);
+}
- /**
- * Create an "anchor" for performing dom insertion/removals.
- * This is used in a number of scenarios:
- * - fragment instance
- * - v-html
- * - v-if
- * - v-for
- * - component
- *
- * @param {String} content
- * @param {Boolean} persist - IE trashes empty textNodes on
- * cloneNode(true), so in certain
- * cases the anchor needs to be
- * non-empty to be persisted in
- * templates.
- * @return {Comment|Text}
- */
-
- function createAnchor(content, persist) {
- var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : '');
- anchor.__v_anchor = true;
- return anchor;
+function stateMixin (Vue) {
+ // flow somehow has problems with directly declared definition object
+ // when using Object.defineProperty, so we have to procedurally build up
+ // the object here.
+ var dataDef = {};
+ dataDef.get = function () {
+ return this._data
+ };
+ {
+ dataDef.set = function (newData) {
+ warn(
+ 'Avoid replacing instance root $data. ' +
+ 'Use nested data properties instead.',
+ this
+ );
+ };
}
+ Object.defineProperty(Vue.prototype, '$data', dataDef);
- /**
- * Find a component ref attribute that starts with $.
- *
- * @param {Element} node
- * @return {String|undefined}
- */
-
- var refRE = /^v-ref:/;
+ Vue.prototype.$set = set;
+ Vue.prototype.$delete = del;
- function findRef(node) {
- if (node.hasAttributes()) {
- var attrs = node.attributes;
- for (var i = 0, l = attrs.length; i < l; i++) {
- var name = attrs[i].name;
- if (refRE.test(name)) {
- return camelize(name.replace(refRE, ''));
- }
- }
+ Vue.prototype.$watch = function (
+ expOrFn,
+ cb,
+ options
+ ) {
+ var vm = this;
+ options = options || {};
+ options.user = true;
+ var watcher = new Watcher(vm, expOrFn, cb, options);
+ if (options.immediate) {
+ cb.call(vm, watcher.value);
}
- }
-
- /**
- * Map a function to a range of nodes .
- *
- * @param {Node} node
- * @param {Node} end
- * @param {Function} op
- */
-
- function mapNodeRange(node, end, op) {
- var next;
- while (node !== end) {
- next = node.nextSibling;
- op(node);
- node = next;
+ return function unwatchFn () {
+ watcher.teardown();
}
- op(end);
- }
-
- /**
- * Remove a range of nodes with transition, store
- * the nodes in a fragment with correct ordering,
- * and call callback when done.
- *
- * @param {Node} start
- * @param {Node} end
- * @param {Vue} vm
- * @param {DocumentFragment} frag
- * @param {Function} cb
- */
+ };
+}
- function removeNodeRange(start, end, vm, frag, cb) {
- var done = false;
- var removed = 0;
- var nodes = [];
- mapNodeRange(start, end, function (node) {
- if (node === end) done = true;
- nodes.push(node);
- removeWithTransition(node, vm, onRemoved);
- });
- function onRemoved() {
- removed++;
- if (done && removed >= nodes.length) {
- for (var i = 0; i < nodes.length; i++) {
- frag.appendChild(nodes[i]);
- }
- cb && cb();
+function proxy (vm, key) {
+ if (!isReserved(key)) {
+ Object.defineProperty(vm, key, {
+ configurable: true,
+ enumerable: true,
+ get: function proxyGetter () {
+ return vm._data[key]
+ },
+ set: function proxySetter (val) {
+ vm._data[key] = val;
}
- }
- }
-
- /**
- * Check if a node is a DocumentFragment.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function isFragment(node) {
- return node && node.nodeType === 11;
+ });
}
-
- /**
- * Get outerHTML of elements, taking care
- * of SVG elements in IE as well.
- *
- * @param {Element} el
- * @return {String}
- */
-
- function getOuterHTML(el) {
- if (el.outerHTML) {
- return el.outerHTML;
+}
+
+/* */
+
+var VNode = function VNode (
+ tag,
+ data,
+ children,
+ text,
+ elm,
+ ns,
+ context,
+ componentOptions
+) {
+ this.tag = tag;
+ this.data = data;
+ this.children = children;
+ this.text = text;
+ this.elm = elm;
+ this.ns = ns;
+ this.context = context;
+ this.functionalContext = undefined;
+ this.key = data && data.key;
+ this.componentOptions = componentOptions;
+ this.child = undefined;
+ this.parent = undefined;
+ this.raw = false;
+ this.isStatic = false;
+ this.isRootInsert = true;
+ this.isComment = false;
+ this.isCloned = false;
+};
+
+var emptyVNode = function () {
+ var node = new VNode();
+ node.text = '';
+ node.isComment = true;
+ return node
+};
+
+// optimized shallow clone
+// used for static nodes and slot nodes because they may be reused across
+// multiple renders, cloning them avoids errors when DOM manipulations rely
+// on their elm reference.
+function cloneVNode (vnode) {
+ var cloned = new VNode(
+ vnode.tag,
+ vnode.data,
+ vnode.children,
+ vnode.text,
+ vnode.elm,
+ vnode.ns,
+ vnode.context,
+ vnode.componentOptions
+ );
+ cloned.isStatic = vnode.isStatic;
+ cloned.key = vnode.key;
+ cloned.isCloned = true;
+ return cloned
+}
+
+function cloneVNodes (vnodes) {
+ var res = new Array(vnodes.length);
+ for (var i = 0; i < vnodes.length; i++) {
+ res[i] = cloneVNode(vnodes[i]);
+ }
+ return res
+}
+
+/* */
+
+function mergeVNodeHook (def, hookKey, hook, key) {
+ key = key + hookKey;
+ var injectedHash = def.__injected || (def.__injected = {});
+ if (!injectedHash[key]) {
+ injectedHash[key] = true;
+ var oldHook = def[hookKey];
+ if (oldHook) {
+ def[hookKey] = function () {
+ oldHook.apply(this, arguments);
+ hook.apply(this, arguments);
+ };
} else {
- var container = document.createElement('div');
- container.appendChild(el.cloneNode(true));
- return container.innerHTML;
- }
- }
-
- var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i;
- var reservedTagRE = /^(slot|partial|component)$/i;
-
- var isUnknownElement = undefined;
- if ('development' !== 'production') {
- isUnknownElement = function (el, tag) {
- if (tag.indexOf('-') > -1) {
- // http://stackoverflow.com/a/28210364/1070244
- return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement;
+ def[hookKey] = hook;
+ }
+ }
+}
+
+/* */
+
+function updateListeners (
+ on,
+ oldOn,
+ add,
+ remove$$1,
+ vm
+) {
+ var name, cur, old, fn, event, capture;
+ for (name in on) {
+ cur = on[name];
+ old = oldOn[name];
+ if (!cur) {
+ "development" !== 'production' && warn(
+ "Invalid handler for event \"" + name + "\": got " + String(cur),
+ vm
+ );
+ } else if (!old) {
+ capture = name.charAt(0) === '!';
+ event = capture ? name.slice(1) : name;
+ if (Array.isArray(cur)) {
+ add(event, (cur.invoker = arrInvoker(cur)), capture);
} else {
- return (/HTMLUnknownElement/.test(el.toString()) &&
- // Chrome returns unknown for several HTML5 elements.
- // https://code.google.com/p/chromium/issues/detail?id=540526
- // Firefox returns unknown for some "Interactive elements."
- !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag)
- );
- }
- };
- }
-
- /**
- * Check if an element is a component, if yes return its
- * component id.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Object|undefined}
- */
-
- function checkComponentAttr(el, options) {
- var tag = el.tagName.toLowerCase();
- var hasAttrs = el.hasAttributes();
- if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
- if (resolveAsset(options, 'components', tag)) {
- return { id: tag };
+ if (!cur.invoker) {
+ fn = cur;
+ cur = on[name] = {};
+ cur.fn = fn;
+ cur.invoker = fnInvoker(cur);
+ }
+ add(event, cur.invoker, capture);
+ }
+ } else if (cur !== old) {
+ if (Array.isArray(old)) {
+ old.length = cur.length;
+ for (var i = 0; i < old.length; i++) { old[i] = cur[i]; }
+ on[name] = old;
} else {
- var is = hasAttrs && getIsBinding(el, options);
- if (is) {
- return is;
- } else if ('development' !== 'production') {
- var expectedTag = options._componentNameMap && options._componentNameMap[tag];
- if (expectedTag) {
- warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.');
- } else if (isUnknownElement(el, tag)) {
- warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.');
- }
- }
+ old.fn = cur;
+ on[name] = old;
}
- } else if (hasAttrs) {
- return getIsBinding(el, options);
}
}
-
- /**
- * Get "is" binding from an element.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Object|undefined}
- */
-
- function getIsBinding(el, options) {
- // dynamic syntax
- var exp = el.getAttribute('is');
- if (exp != null) {
- if (resolveAsset(options, 'components', exp)) {
- el.removeAttribute('is');
- return { id: exp };
- }
- } else {
- exp = getBindAttr(el, 'is');
- if (exp != null) {
- return { id: exp, dynamic: true };
- }
+ for (name in oldOn) {
+ if (!on[name]) {
+ event = name.charAt(0) === '!' ? name.slice(1) : name;
+ remove$$1(event, oldOn[name].invoker);
}
}
+}
- /**
- * Option overwriting strategies are functions that handle
- * how to merge a parent option value and a child option
- * value into the final value.
- *
- * All strategy functions follow the same signature:
- *
- * @param {*} parentVal
- * @param {*} childVal
- * @param {Vue} [vm]
- */
-
- var strats = config.optionMergeStrategies = Object.create(null);
+function arrInvoker (arr) {
+ return function (ev) {
+ var arguments$1 = arguments;
- /**
- * Helper that recursively merges two data objects together.
- */
-
- function mergeData(to, from) {
- var key, toVal, fromVal;
- for (key in from) {
- toVal = to[key];
- fromVal = from[key];
- if (!hasOwn(to, key)) {
- set(to, key, fromVal);
- } else if (isObject(toVal) && isObject(fromVal)) {
- mergeData(toVal, fromVal);
- }
+ var single = arguments.length === 1;
+ for (var i = 0; i < arr.length; i++) {
+ single ? arr[i](ev) : arr[i].apply(null, arguments$1);
}
- return to;
}
+}
- /**
- * Data
- */
+function fnInvoker (o) {
+ return function (ev) {
+ var single = arguments.length === 1;
+ single ? o.fn(ev) : o.fn.apply(null, arguments);
+ }
+}
- strats.data = function (parentVal, childVal, vm) {
- if (!vm) {
- // in a Vue.extend merge, both should be functions
- if (!childVal) {
- return parentVal;
- }
- if (typeof childVal !== 'function') {
- 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
- return parentVal;
- }
- if (!parentVal) {
- return childVal;
- }
- // when parentVal & childVal are both present,
- // we need to return a function that returns the
- // merged result of both functions... no need to
- // check if parentVal is a function here because
- // it has to be a function to pass previous merges.
- return function mergedDataFn() {
- return mergeData(childVal.call(this), parentVal.call(this));
- };
- } else if (parentVal || childVal) {
- return function mergedInstanceDataFn() {
- // instance merge
- var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal;
- var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined;
- if (instanceData) {
- return mergeData(instanceData, defaultData);
+/* */
+
+function normalizeChildren (
+ children,
+ ns,
+ nestedIndex
+) {
+ if (isPrimitive(children)) {
+ return [createTextVNode(children)]
+ }
+ if (Array.isArray(children)) {
+ var res = [];
+ for (var i = 0, l = children.length; i < l; i++) {
+ var c = children[i];
+ var last = res[res.length - 1];
+ // nested
+ if (Array.isArray(c)) {
+ res.push.apply(res, normalizeChildren(c, ns, ((nestedIndex || '') + "_" + i)));
+ } else if (isPrimitive(c)) {
+ if (last && last.text) {
+ last.text += String(c);
+ } else if (c !== '') {
+ // convert primitive to vnode
+ res.push(createTextVNode(c));
+ }
+ } else if (c instanceof VNode) {
+ if (c.text && last && last.text) {
+ last.text += c.text;
} else {
- return defaultData;
+ // inherit parent namespace
+ if (ns) {
+ applyNS(c, ns);
+ }
+ // default key for nested array children (likely generated by v-for)
+ if (c.tag && c.key == null && nestedIndex != null) {
+ c.key = "__vlist" + nestedIndex + "_" + i + "__";
+ }
+ res.push(c);
}
- };
- }
- };
-
- /**
- * El
- */
-
- strats.el = function (parentVal, childVal, vm) {
- if (!vm && childVal && typeof childVal !== 'function') {
- 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm);
- return;
+ }
}
- var ret = childVal || parentVal;
- // invoke the element factory if this is instance merge
- return vm && typeof ret === 'function' ? ret.call(vm) : ret;
- };
-
- /**
- * Hooks and param attributes are merged as arrays.
- */
-
- strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) {
- return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal;
- };
-
- /**
- * Assets
- *
- * When a vm is present (instance creation), we need to do
- * a three-way merge between constructor options, instance
- * options and parent options.
- */
-
- function mergeAssets(parentVal, childVal) {
- var res = Object.create(parentVal || null);
- return childVal ? extend(res, guardArrayAssets(childVal)) : res;
+ return res
}
+}
- config._assetTypes.forEach(function (type) {
- strats[type + 's'] = mergeAssets;
- });
-
- /**
- * Events & Watchers.
- *
- * Events & watchers hashes should not overwrite one
- * another, so we merge them as arrays.
- */
+function createTextVNode (val) {
+ return new VNode(undefined, undefined, undefined, String(val))
+}
- strats.watch = strats.events = function (parentVal, childVal) {
- if (!childVal) return parentVal;
- if (!parentVal) return childVal;
- var ret = {};
- extend(ret, parentVal);
- for (var key in childVal) {
- var parent = ret[key];
- var child = childVal[key];
- if (parent && !isArray(parent)) {
- parent = [parent];
+function applyNS (vnode, ns) {
+ if (vnode.tag && !vnode.ns) {
+ vnode.ns = ns;
+ if (vnode.children) {
+ for (var i = 0, l = vnode.children.length; i < l; i++) {
+ applyNS(vnode.children[i], ns);
}
- ret[key] = parent ? parent.concat(child) : [child];
}
- return ret;
- };
+ }
+}
- /**
- * Other object hashes.
- */
+/* */
- strats.props = strats.methods = strats.computed = function (parentVal, childVal) {
- if (!childVal) return parentVal;
- if (!parentVal) return childVal;
- var ret = Object.create(null);
- extend(ret, parentVal);
- extend(ret, childVal);
- return ret;
- };
+function getFirstComponentChild (children) {
+ return children && children.filter(function (c) { return c && c.componentOptions; })[0]
+}
- /**
- * Default strategy.
- */
+/* */
- var defaultStrat = function defaultStrat(parentVal, childVal) {
- return childVal === undefined ? parentVal : childVal;
- };
+var activeInstance = null;
- /**
- * Make sure component options get converted to actual
- * constructors.
- *
- * @param {Object} options
- */
+function initLifecycle (vm) {
+ var options = vm.$options;
- function guardComponents(options) {
- if (options.components) {
- var components = options.components = guardArrayAssets(options.components);
- var ids = Object.keys(components);
- var def;
- if ('development' !== 'production') {
- var map = options._componentNameMap = {};
- }
- for (var i = 0, l = ids.length; i < l; i++) {
- var key = ids[i];
- if (commonTagRE.test(key) || reservedTagRE.test(key)) {
- 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key);
- continue;
- }
- // record a all lowercase <-> kebab-case mapping for
- // possible custom element case error warning
- if ('development' !== 'production') {
- map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key);
- }
- def = components[key];
- if (isPlainObject(def)) {
- components[key] = Vue.extend(def);
- }
- }
+ // locate first non-abstract parent
+ var parent = options.parent;
+ if (parent && !options.abstract) {
+ while (parent.$options.abstract && parent.$parent) {
+ parent = parent.$parent;
}
+ parent.$children.push(vm);
}
- /**
- * Ensure all props option syntax are normalized into the
- * Object-based format.
- *
- * @param {Object} options
- */
+ vm.$parent = parent;
+ vm.$root = parent ? parent.$root : vm;
- function guardProps(options) {
- var props = options.props;
- var i, val;
- if (isArray(props)) {
- options.props = {};
- i = props.length;
- while (i--) {
- val = props[i];
- if (typeof val === 'string') {
- options.props[val] = null;
- } else if (val.name) {
- options.props[val.name] = val;
- }
- }
- } else if (isPlainObject(props)) {
- var keys = Object.keys(props);
- i = keys.length;
- while (i--) {
- val = props[keys[i]];
- if (typeof val === 'function') {
- props[keys[i]] = { type: val };
- }
- }
- }
- }
+ vm.$children = [];
+ vm.$refs = {};
- /**
- * Guard an Array-format assets option and converted it
- * into the key-value Object format.
- *
- * @param {Object|Array} assets
- * @return {Object}
- */
+ vm._watcher = null;
+ vm._inactive = false;
+ vm._isMounted = false;
+ vm._isDestroyed = false;
+ vm._isBeingDestroyed = false;
+}
- function guardArrayAssets(assets) {
- if (isArray(assets)) {
- var res = {};
- var i = assets.length;
- var asset;
- while (i--) {
- asset = assets[i];
- var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id;
- if (!id) {
- 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.');
+function lifecycleMixin (Vue) {
+ Vue.prototype._mount = function (
+ el,
+ hydrating
+ ) {
+ var vm = this;
+ vm.$el = el;
+ if (!vm.$options.render) {
+ vm.$options.render = emptyVNode;
+ {
+ /* istanbul ignore if */
+ if (vm.$options.template) {
+ warn(
+ 'You are using the runtime-only build of Vue where the template ' +
+ 'option is not available. Either pre-compile the templates into ' +
+ 'render functions, or use the compiler-included build.',
+ vm
+ );
} else {
- res[id] = asset;
+ warn(
+ 'Failed to mount component: template or render function not defined.',
+ vm
+ );
}
}
- return res;
}
- return assets;
- }
-
- /**
- * Merge two option objects into a new one.
- * Core utility used in both instantiation and inheritance.
- *
- * @param {Object} parent
- * @param {Object} child
- * @param {Vue} [vm] - if vm is present, indicates this is
- * an instantiation merge.
- */
-
- function mergeOptions(parent, child, vm) {
- guardComponents(child);
- guardProps(child);
- if ('development' !== 'production') {
- if (child.propsData && !vm) {
- warn('propsData can only be used as an instantiation option.');
- }
+ callHook(vm, 'beforeMount');
+ vm._watcher = new Watcher(vm, function () {
+ vm._update(vm._render(), hydrating);
+ }, noop);
+ hydrating = false;
+ // manually mounted instance, call mounted on self
+ // mounted is called for render-created child components in its inserted hook
+ if (vm.$vnode == null) {
+ vm._isMounted = true;
+ callHook(vm, 'mounted');
}
- var options = {};
- var key;
- if (child['extends']) {
- parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
+ return vm
+ };
+
+ Vue.prototype._update = function (vnode, hydrating) {
+ var vm = this;
+ if (vm._isMounted) {
+ callHook(vm, 'beforeUpdate');
+ }
+ var prevEl = vm.$el;
+ var prevActiveInstance = activeInstance;
+ activeInstance = vm;
+ var prevVnode = vm._vnode;
+ vm._vnode = vnode;
+ if (!prevVnode) {
+ // Vue.prototype.__patch__ is injected in entry points
+ // based on the rendering backend used.
+ vm.$el = vm.__patch__(vm.$el, vnode, hydrating);
+ } else {
+ vm.$el = vm.__patch__(prevVnode, vnode);
}
- if (child.mixins) {
- for (var i = 0, l = child.mixins.length; i < l; i++) {
- var mixin = child.mixins[i];
- var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin;
- parent = mergeOptions(parent, mixinOptions, vm);
- }
+ activeInstance = prevActiveInstance;
+ // update __vue__ reference
+ if (prevEl) {
+ prevEl.__vue__ = null;
}
- for (key in parent) {
- mergeField(key);
+ if (vm.$el) {
+ vm.$el.__vue__ = vm;
}
- for (key in child) {
- if (!hasOwn(parent, key)) {
- mergeField(key);
- }
+ // if parent is an HOC, update its $el as well
+ if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
+ vm.$parent.$el = vm.$el;
}
- function mergeField(key) {
- var strat = strats[key] || defaultStrat;
- options[key] = strat(parent[key], child[key], vm, key);
+ if (vm._isMounted) {
+ callHook(vm, 'updated');
}
- return options;
- }
-
- /**
- * Resolve an asset.
- * This function is used because child instances need access
- * to assets defined in its ancestor chain.
- *
- * @param {Object} options
- * @param {String} type
- * @param {String} id
- * @param {Boolean} warnMissing
- * @return {Object|Function}
- */
+ };
- function resolveAsset(options, type, id, warnMissing) {
- /* istanbul ignore if */
- if (typeof id !== 'string') {
- return;
+ Vue.prototype._updateFromParent = function (
+ propsData,
+ listeners,
+ parentVnode,
+ renderChildren
+ ) {
+ var vm = this;
+ var hasChildren = !!(vm.$options._renderChildren || renderChildren);
+ vm.$options._parentVnode = parentVnode;
+ vm.$options._renderChildren = renderChildren;
+ // update props
+ if (propsData && vm.$options.props) {
+ observerState.shouldConvert = false;
+ {
+ observerState.isSettingProps = true;
+ }
+ var propKeys = vm.$options._propKeys || [];
+ for (var i = 0; i < propKeys.length; i++) {
+ var key = propKeys[i];
+ vm[key] = validateProp(key, vm.$options.props, propsData, vm);
+ }
+ observerState.shouldConvert = true;
+ {
+ observerState.isSettingProps = false;
+ }
}
- var assets = options[type];
- var camelizedId;
- var res = assets[id] ||
- // camelCase ID
- assets[camelizedId = camelize(id)] ||
- // Pascal Case ID
- assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)];
- if ('development' !== 'production' && warnMissing && !res) {
- warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
+ // update listeners
+ if (listeners) {
+ var oldListeners = vm.$options._parentListeners;
+ vm.$options._parentListeners = listeners;
+ vm._updateListeners(listeners, oldListeners);
}
- return res;
- }
-
- var uid$1 = 0;
-
- /**
- * A dep is an observable that can have multiple
- * directives subscribing to it.
- *
- * @constructor
- */
- function Dep() {
- this.id = uid$1++;
- this.subs = [];
- }
-
- // the current target watcher being evaluated.
- // this is globally unique because there could be only one
- // watcher being evaluated at any time.
- Dep.target = null;
-
- /**
- * Add a directive subscriber.
- *
- * @param {Directive} sub
- */
-
- Dep.prototype.addSub = function (sub) {
- this.subs.push(sub);
- };
-
- /**
- * Remove a directive subscriber.
- *
- * @param {Directive} sub
- */
-
- Dep.prototype.removeSub = function (sub) {
- this.subs.$remove(sub);
- };
-
- /**
- * Add self as a dependency to the target watcher.
- */
-
- Dep.prototype.depend = function () {
- Dep.target.addDep(this);
- };
-
- /**
- * Notify all subscribers of a new value.
- */
-
- Dep.prototype.notify = function () {
- // stablize the subscriber list first
- var subs = toArray(this.subs);
- for (var i = 0, l = subs.length; i < l; i++) {
- subs[i].update();
+ // resolve slots + force update if has children
+ if (hasChildren) {
+ vm.$slots = resolveSlots(renderChildren, vm._renderContext);
+ vm.$forceUpdate();
}
};
- var arrayProto = Array.prototype;
- var arrayMethods = Object.create(arrayProto)
-
- /**
- * Intercept mutating methods and emit events
- */
-
- ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
- // cache original method
- var original = arrayProto[method];
- def(arrayMethods, method, function mutator() {
- // avoid leaking arguments:
- // http://jsperf.com/closure-with-arguments
- var i = arguments.length;
- var args = new Array(i);
- while (i--) {
- args[i] = arguments[i];
- }
- var result = original.apply(this, args);
- var ob = this.__ob__;
- var inserted;
- switch (method) {
- case 'push':
- inserted = args;
- break;
- case 'unshift':
- inserted = args;
- break;
- case 'splice':
- inserted = args.slice(2);
- break;
- }
- if (inserted) ob.observeArray(inserted);
- // notify change
- ob.dep.notify();
- return result;
- });
- });
-
- /**
- * Swap the element at the given index with a new value
- * and emits corresponding event.
- *
- * @param {Number} index
- * @param {*} val
- * @return {*} - replaced element
- */
-
- def(arrayProto, '$set', function $set(index, val) {
- if (index >= this.length) {
- this.length = Number(index) + 1;
+ Vue.prototype.$forceUpdate = function () {
+ var vm = this;
+ if (vm._watcher) {
+ vm._watcher.update();
}
- return this.splice(index, 1, val)[0];
- });
-
- /**
- * Convenience method to remove the element at given index or target element reference.
- *
- * @param {*} item
- */
+ };
- def(arrayProto, '$remove', function $remove(item) {
- /* istanbul ignore if */
- if (!this.length) return;
- var index = indexOf(this, item);
- if (index > -1) {
- return this.splice(index, 1);
+ Vue.prototype.$destroy = function () {
+ var vm = this;
+ if (vm._isBeingDestroyed) {
+ return
}
- });
-
- var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
-
- /**
- * By default, when a reactive property is set, the new value is
- * also converted to become reactive. However in certain cases, e.g.
- * v-for scope alias and props, we don't want to force conversion
- * because the value may be a nested value under a frozen data structure.
- *
- * So whenever we want to set a reactive property without forcing
- * conversion on the new value, we wrap that call inside this function.
- */
-
- var shouldConvert = true;
-
- function withoutConversion(fn) {
- shouldConvert = false;
- fn();
- shouldConvert = true;
- }
-
- /**
- * Observer class that are attached to each observed
- * object. Once attached, the observer converts target
- * object's property keys into getter/setters that
- * collect dependencies and dispatches updates.
- *
- * @param {Array|Object} value
- * @constructor
- */
-
- function Observer(value) {
- this.value = value;
- this.dep = new Dep();
- def(value, '__ob__', this);
- if (isArray(value)) {
- var augment = hasProto ? protoAugment : copyAugment;
- augment(value, arrayMethods, arrayKeys);
- this.observeArray(value);
- } else {
- this.walk(value);
+ callHook(vm, 'beforeDestroy');
+ vm._isBeingDestroyed = true;
+ // remove self from parent
+ var parent = vm.$parent;
+ if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
+ remove$1(parent.$children, vm);
}
- }
-
- // Instance methods
-
- /**
- * Walk through each property and convert them into
- * getter/setters. This method should only be called when
- * value type is Object.
- *
- * @param {Object} obj
- */
-
- Observer.prototype.walk = function (obj) {
- var keys = Object.keys(obj);
- for (var i = 0, l = keys.length; i < l; i++) {
- this.convert(keys[i], obj[keys[i]]);
+ // teardown watchers
+ if (vm._watcher) {
+ vm._watcher.teardown();
}
+ var i = vm._watchers.length;
+ while (i--) {
+ vm._watchers[i].teardown();
+ }
+ // remove reference from data ob
+ // frozen object may not have observer.
+ if (vm._data.__ob__) {
+ vm._data.__ob__.vmCount--;
+ }
+ // call the last hook...
+ vm._isDestroyed = true;
+ callHook(vm, 'destroyed');
+ // turn off all instance listeners.
+ vm.$off();
+ // remove __vue__ reference
+ if (vm.$el) {
+ vm.$el.__vue__ = null;
+ }
+ // invoke destroy hooks on current rendered tree
+ vm.__patch__(vm._vnode, null);
};
+}
- /**
- * Observe a list of Array items.
- *
- * @param {Array} items
- */
-
- Observer.prototype.observeArray = function (items) {
- for (var i = 0, l = items.length; i < l; i++) {
- observe(items[i]);
+function callHook (vm, hook) {
+ var handlers = vm.$options[hook];
+ if (handlers) {
+ for (var i = 0, j = handlers.length; i < j; i++) {
+ handlers[i].call(vm);
}
- };
-
- /**
- * Convert a property into getter/setter so we can emit
- * the events when the property is accessed/changed.
- *
- * @param {String} key
- * @param {*} val
- */
-
- Observer.prototype.convert = function (key, val) {
- defineReactive(this.value, key, val);
- };
-
- /**
- * Add an owner vm, so that when $set/$delete mutations
- * happen we can notify owner vms to proxy the keys and
- * digest the watchers. This is only called when the object
- * is observed as an instance's root $data.
- *
- * @param {Vue} vm
- */
-
- Observer.prototype.addVm = function (vm) {
- (this.vms || (this.vms = [])).push(vm);
- };
-
- /**
- * Remove an owner vm. This is called when the object is
- * swapped out as an instance's $data object.
- *
- * @param {Vue} vm
- */
-
- Observer.prototype.removeVm = function (vm) {
- this.vms.$remove(vm);
- };
+ }
+ vm.$emit('hook:' + hook);
+}
- // helpers
+/* */
- /**
- * Augment an target Object or Array by intercepting
- * the prototype chain using __proto__
- *
- * @param {Object|Array} target
- * @param {Object} src
- */
+var hooks = { init: init, prepatch: prepatch, insert: insert, destroy: destroy$1 };
+var hooksToMerge = Object.keys(hooks);
- function protoAugment(target, src) {
- /* eslint-disable no-proto */
- target.__proto__ = src;
- /* eslint-enable no-proto */
+function createComponent (
+ Ctor,
+ data,
+ context,
+ children,
+ tag
+) {
+ if (!Ctor) {
+ return
}
- /**
- * Augment an target Object or Array by defining
- * hidden properties.
- *
- * @param {Object|Array} target
- * @param {Object} proto
- */
-
- function copyAugment(target, src, keys) {
- for (var i = 0, l = keys.length; i < l; i++) {
- var key = keys[i];
- def(target, key, src[key]);
- }
+ if (isObject(Ctor)) {
+ Ctor = Vue$3.extend(Ctor);
}
- /**
- * Attempt to create an observer instance for a value,
- * returns the new observer if successfully observed,
- * or the existing observer if the value already has one.
- *
- * @param {*} value
- * @param {Vue} [vm]
- * @return {Observer|undefined}
- * @static
- */
-
- function observe(value, vm) {
- if (!value || typeof value !== 'object') {
- return;
- }
- var ob;
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__;
- } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
- ob = new Observer(value);
- }
- if (ob && vm) {
- ob.addVm(vm);
+ if (typeof Ctor !== 'function') {
+ {
+ warn(("Invalid Component definition: " + (String(Ctor))), context);
}
- return ob;
+ return
}
- /**
- * Define a reactive property on an Object.
- *
- * @param {Object} obj
- * @param {String} key
- * @param {*} val
- */
-
- function defineReactive(obj, key, val) {
- var dep = new Dep();
-
- var property = Object.getOwnPropertyDescriptor(obj, key);
- if (property && property.configurable === false) {
- return;
+ // async component
+ if (!Ctor.cid) {
+ if (Ctor.resolved) {
+ Ctor = Ctor.resolved;
+ } else {
+ Ctor = resolveAsyncComponent(Ctor, function () {
+ // it's ok to queue this on every render because
+ // $forceUpdate is buffered by the scheduler.
+ context.$forceUpdate();
+ });
+ if (!Ctor) {
+ // return nothing if this is indeed an async component
+ // wait for the callback to trigger parent update.
+ return
+ }
+ }
+ }
+
+ data = data || {};
+
+ // extract props
+ var propsData = extractProps(data, Ctor);
+
+ // functional component
+ if (Ctor.options.functional) {
+ return createFunctionalComponent(Ctor, propsData, data, context, children)
+ }
+
+ // extract listeners, since these needs to be treated as
+ // child component listeners instead of DOM listeners
+ var listeners = data.on;
+ // replace with listeners with .native modifier
+ data.on = data.nativeOn;
+
+ if (Ctor.options.abstract) {
+ // abstract components do not keep anything
+ // other than props & listeners
+ data = {};
+ }
+
+ // merge component management hooks onto the placeholder node
+ mergeHooks(data);
+
+ // return a placeholder vnode
+ var name = Ctor.options.name || tag;
+ var vnode = new VNode(
+ ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
+ data, undefined, undefined, undefined, undefined, context,
+ { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
+ );
+ return vnode
+}
+
+function createFunctionalComponent (
+ Ctor,
+ propsData,
+ data,
+ context,
+ children
+) {
+ var props = {};
+ var propOptions = Ctor.options.props;
+ if (propOptions) {
+ for (var key in propOptions) {
+ props[key] = validateProp(key, propOptions, propsData);
+ }
+ }
+ var vnode = Ctor.options.render.call(
+ null,
+ // ensure the createElement function in functional components
+ // gets a unique context - this is necessary for correct named slot check
+ bind$1(createElement, { _self: Object.create(context) }),
+ {
+ props: props,
+ data: data,
+ parent: context,
+ children: normalizeChildren(children),
+ slots: function () { return resolveSlots(children, context); }
+ }
+ );
+ if (vnode instanceof VNode) {
+ vnode.functionalContext = context;
+ if (data.slot) {
+ (vnode.data || (vnode.data = {})).slot = data.slot;
+ }
+ }
+ return vnode
+}
+
+function createComponentInstanceForVnode (
+ vnode, // we know it's MountedComponentVNode but flow doesn't
+ parent // activeInstance in lifecycle state
+) {
+ var vnodeComponentOptions = vnode.componentOptions;
+ var options = {
+ _isComponent: true,
+ parent: parent,
+ propsData: vnodeComponentOptions.propsData,
+ _componentTag: vnodeComponentOptions.tag,
+ _parentVnode: vnode,
+ _parentListeners: vnodeComponentOptions.listeners,
+ _renderChildren: vnodeComponentOptions.children
+ };
+ // check inline-template render functions
+ var inlineTemplate = vnode.data.inlineTemplate;
+ if (inlineTemplate) {
+ options.render = inlineTemplate.render;
+ options.staticRenderFns = inlineTemplate.staticRenderFns;
+ }
+ return new vnodeComponentOptions.Ctor(options)
+}
+
+function init (vnode, hydrating) {
+ if (!vnode.child || vnode.child._isDestroyed) {
+ var child = vnode.child = createComponentInstanceForVnode(vnode, activeInstance);
+ child.$mount(hydrating ? vnode.elm : undefined, hydrating);
+ }
+}
+
+function prepatch (
+ oldVnode,
+ vnode
+) {
+ var options = vnode.componentOptions;
+ var child = vnode.child = oldVnode.child;
+ child._updateFromParent(
+ options.propsData, // updated props
+ options.listeners, // updated listeners
+ vnode, // new parent vnode
+ options.children // new children
+ );
+}
+
+function insert (vnode) {
+ if (!vnode.child._isMounted) {
+ vnode.child._isMounted = true;
+ callHook(vnode.child, 'mounted');
+ }
+ if (vnode.data.keepAlive) {
+ vnode.child._inactive = false;
+ callHook(vnode.child, 'activated');
+ }
+}
+
+function destroy$1 (vnode) {
+ if (!vnode.child._isDestroyed) {
+ if (!vnode.data.keepAlive) {
+ vnode.child.$destroy();
+ } else {
+ vnode.child._inactive = true;
+ callHook(vnode.child, 'deactivated');
}
-
- // cater for pre-defined getter/setters
- var getter = property && property.get;
- var setter = property && property.set;
-
- var childOb = observe(val);
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter() {
- var value = getter ? getter.call(obj) : val;
- if (Dep.target) {
- dep.depend();
- if (childOb) {
- childOb.dep.depend();
- }
- if (isArray(value)) {
- for (var e, i = 0, l = value.length; i < l; i++) {
- e = value[i];
- e && e.__ob__ && e.__ob__.dep.depend();
- }
- }
- }
- return value;
- },
- set: function reactiveSetter(newVal) {
- var value = getter ? getter.call(obj) : val;
- if (newVal === value) {
- return;
- }
- if (setter) {
- setter.call(obj, newVal);
- } else {
- val = newVal;
- }
- childOb = observe(newVal);
- dep.notify();
- }
- });
}
+}
-
-
- var util = Object.freeze({
- defineReactive: defineReactive,
- set: set,
- del: del,
- hasOwn: hasOwn,
- isLiteral: isLiteral,
- isReserved: isReserved,
- _toString: _toString,
- toNumber: toNumber,
- toBoolean: toBoolean,
- stripQuotes: stripQuotes,
- camelize: camelize,
- hyphenate: hyphenate,
- classify: classify,
- bind: bind,
- toArray: toArray,
- extend: extend,
- isObject: isObject,
- isPlainObject: isPlainObject,
- def: def,
- debounce: _debounce,
- indexOf: indexOf,
- cancellable: cancellable,
- looseEqual: looseEqual,
- isArray: isArray,
- hasProto: hasProto,
- inBrowser: inBrowser,
- devtools: devtools,
- isIE: isIE,
- isIE9: isIE9,
- isAndroid: isAndroid,
- isIos: isIos,
- iosVersionMatch: iosVersionMatch,
- iosVersion: iosVersion,
- hasMutationObserverBug: hasMutationObserverBug,
- get transitionProp () { return transitionProp; },
- get transitionEndEvent () { return transitionEndEvent; },
- get animationProp () { return animationProp; },
- get animationEndEvent () { return animationEndEvent; },
- nextTick: nextTick,
- get _Set () { return _Set; },
- query: query,
- inDoc: inDoc,
- getAttr: getAttr,
- getBindAttr: getBindAttr,
- hasBindAttr: hasBindAttr,
- before: before,
- after: after,
- remove: remove,
- prepend: prepend,
- replace: replace,
- on: on,
- off: off,
- setClass: setClass,
- addClass: addClass,
- removeClass: removeClass,
- extractContent: extractContent,
- trimNode: trimNode,
- isTemplate: isTemplate,
- createAnchor: createAnchor,
- findRef: findRef,
- mapNodeRange: mapNodeRange,
- removeNodeRange: removeNodeRange,
- isFragment: isFragment,
- getOuterHTML: getOuterHTML,
- mergeOptions: mergeOptions,
- resolveAsset: resolveAsset,
- checkComponentAttr: checkComponentAttr,
- commonTagRE: commonTagRE,
- reservedTagRE: reservedTagRE,
- get warn () { return warn; }
- });
-
- var uid = 0;
-
- function initMixin (Vue) {
- /**
- * The main init sequence. This is called for every
- * instance, including ones that are created from extended
- * constructors.
- *
- * @param {Object} options - this options object should be
- * the result of merging class
- * options and the options passed
- * in to the constructor.
- */
-
- Vue.prototype._init = function (options) {
- options = options || {};
-
- this.$el = null;
- this.$parent = options.parent;
- this.$root = this.$parent ? this.$parent.$root : this;
- this.$children = [];
- this.$refs = {}; // child vm references
- this.$els = {}; // element references
- this._watchers = []; // all watchers as an array
- this._directives = []; // all directives
-
- // a uid
- this._uid = uid++;
-
- // a flag to avoid this being observed
- this._isVue = true;
-
- // events bookkeeping
- this._events = {}; // registered callbacks
- this._eventsCount = {}; // for $broadcast optimization
-
- // fragment instance properties
- this._isFragment = false;
- this._fragment = // @type {DocumentFragment}
- this._fragmentStart = // @type {Text|Comment}
- this._fragmentEnd = null; // @type {Text|Comment}
-
- // lifecycle state
- this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false;
- this._unlinkFn = null;
-
- // context:
- // if this is a transcluded component, context
- // will be the common parent vm of this instance
- // and its host.
- this._context = options._context || this.$parent;
-
- // scope:
- // if this is inside an inline v-for, the scope
- // will be the intermediate scope created for this
- // repeat fragment. this is used for linking props
- // and container directives.
- this._scope = options._scope;
-
- // fragment:
- // if this instance is compiled inside a Fragment, it
- // needs to reigster itself as a child of that fragment
- // for attach/detach to work properly.
- this._frag = options._frag;
- if (this._frag) {
- this._frag.children.push(this);
- }
-
- // push self into parent / transclusion host
- if (this.$parent) {
- this.$parent.$children.push(this);
- }
-
- // merge options.
- options = this.$options = mergeOptions(this.constructor.options, options, this);
-
- // set ref
- this._updateRef();
-
- // initialize data as empty object.
- // it will be filled up in _initData().
- this._data = {};
-
- // call init hook
- this._callHook('init');
-
- // initialize data observation and scope inheritance.
- this._initState();
-
- // setup event system and option events.
- this._initEvents();
-
- // call created hook
- this._callHook('created');
-
- // if `el` option is passed, start compilation.
- if (options.el) {
- this.$mount(options.el);
+function resolveAsyncComponent (
+ factory,
+ cb
+) {
+ if (factory.requested) {
+ // pool callbacks
+ factory.pendingCallbacks.push(cb);
+ } else {
+ factory.requested = true;
+ var cbs = factory.pendingCallbacks = [cb];
+ var sync = true;
+
+ var resolve = function (res) {
+ if (isObject(res)) {
+ res = Vue$3.extend(res);
+ }
+ // cache resolved
+ factory.resolved = res;
+ // invoke callbacks only if this is not a synchronous resolve
+ // (async resolves are shimmed as synchronous during SSR)
+ if (!sync) {
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ cbs[i](res);
+ }
}
};
- }
-
- var pathCache = new Cache(1000);
-
- // actions
- var APPEND = 0;
- var PUSH = 1;
- var INC_SUB_PATH_DEPTH = 2;
- var PUSH_SUB_PATH = 3;
-
- // states
- var BEFORE_PATH = 0;
- var IN_PATH = 1;
- var BEFORE_IDENT = 2;
- var IN_IDENT = 3;
- var IN_SUB_PATH = 4;
- var IN_SINGLE_QUOTE = 5;
- var IN_DOUBLE_QUOTE = 6;
- var AFTER_PATH = 7;
- var ERROR = 8;
-
- var pathStateMachine = [];
-
- pathStateMachine[BEFORE_PATH] = {
- 'ws': [BEFORE_PATH],
- 'ident': [IN_IDENT, APPEND],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- };
- pathStateMachine[IN_PATH] = {
- 'ws': [IN_PATH],
- '.': [BEFORE_IDENT],
- '[': [IN_SUB_PATH],
- 'eof': [AFTER_PATH]
- };
-
- pathStateMachine[BEFORE_IDENT] = {
- 'ws': [BEFORE_IDENT],
- 'ident': [IN_IDENT, APPEND]
- };
-
- pathStateMachine[IN_IDENT] = {
- 'ident': [IN_IDENT, APPEND],
- '0': [IN_IDENT, APPEND],
- 'number': [IN_IDENT, APPEND],
- 'ws': [IN_PATH, PUSH],
- '.': [BEFORE_IDENT, PUSH],
- '[': [IN_SUB_PATH, PUSH],
- 'eof': [AFTER_PATH, PUSH]
- };
-
- pathStateMachine[IN_SUB_PATH] = {
- "'": [IN_SINGLE_QUOTE, APPEND],
- '"': [IN_DOUBLE_QUOTE, APPEND],
- '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH],
- ']': [IN_PATH, PUSH_SUB_PATH],
- 'eof': ERROR,
- 'else': [IN_SUB_PATH, APPEND]
- };
-
- pathStateMachine[IN_SINGLE_QUOTE] = {
- "'": [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_SINGLE_QUOTE, APPEND]
- };
-
- pathStateMachine[IN_DOUBLE_QUOTE] = {
- '"': [IN_SUB_PATH, APPEND],
- 'eof': ERROR,
- 'else': [IN_DOUBLE_QUOTE, APPEND]
- };
-
- /**
- * Determine the type of a character in a keypath.
- *
- * @param {Char} ch
- * @return {String} type
- */
-
- function getPathCharType(ch) {
- if (ch === undefined) {
- return 'eof';
- }
-
- var code = ch.charCodeAt(0);
-
- switch (code) {
- case 0x5B: // [
- case 0x5D: // ]
- case 0x2E: // .
- case 0x22: // "
- case 0x27: // '
- case 0x30:
- // 0
- return ch;
-
- case 0x5F: // _
- case 0x24:
- // $
- return 'ident';
+ var reject = function (reason) {
+ "development" !== 'production' && warn(
+ "Failed to resolve async component: " + (String(factory)) +
+ (reason ? ("\nReason: " + reason) : '')
+ );
+ };
- case 0x20: // Space
- case 0x09: // Tab
- case 0x0A: // Newline
- case 0x0D: // Return
- case 0xA0: // No-break space
- case 0xFEFF: // Byte Order Mark
- case 0x2028: // Line Separator
- case 0x2029:
- // Paragraph Separator
- return 'ws';
+ var res = factory(resolve, reject);
+
+ // handle promise
+ if (res && typeof res.then === 'function' && !factory.resolved) {
+ res.then(resolve, reject);
+ }
+
+ sync = false;
+ // return in case resolved synchronously
+ return factory.resolved
+ }
+}
+
+function extractProps (data, Ctor) {
+ // we are only extrating raw values here.
+ // validation and default values are handled in the child
+ // component itself.
+ var propOptions = Ctor.options.props;
+ if (!propOptions) {
+ return
+ }
+ var res = {};
+ var attrs = data.attrs;
+ var props = data.props;
+ var domProps = data.domProps;
+ if (attrs || props || domProps) {
+ for (var key in propOptions) {
+ var altKey = hyphenate(key);
+ checkProp(res, props, key, altKey, true) ||
+ checkProp(res, attrs, key, altKey) ||
+ checkProp(res, domProps, key, altKey);
+ }
+ }
+ return res
+}
+
+function checkProp (
+ res,
+ hash,
+ key,
+ altKey,
+ preserve
+) {
+ if (hash) {
+ if (hasOwn(hash, key)) {
+ res[key] = hash[key];
+ if (!preserve) {
+ delete hash[key];
+ }
+ return true
+ } else if (hasOwn(hash, altKey)) {
+ res[key] = hash[altKey];
+ if (!preserve) {
+ delete hash[altKey];
+ }
+ return true
+ }
+ }
+ return false
+}
+
+function mergeHooks (data) {
+ if (!data.hook) {
+ data.hook = {};
+ }
+ for (var i = 0; i < hooksToMerge.length; i++) {
+ var key = hooksToMerge[i];
+ var fromParent = data.hook[key];
+ var ours = hooks[key];
+ data.hook[key] = fromParent ? mergeHook$1(ours, fromParent) : ours;
+ }
+}
+
+function mergeHook$1 (a, b) {
+ // since all hooks have at most two args, use fixed args
+ // to avoid having to use fn.apply().
+ return function (_, __) {
+ a(_, __);
+ b(_, __);
+ }
+}
+
+/* */
+
+// wrapper function for providing a more flexible interface
+// without getting yelled at by flow
+function createElement (
+ tag,
+ data,
+ children
+) {
+ if (data && (Array.isArray(data) || typeof data !== 'object')) {
+ children = data;
+ data = undefined;
+ }
+ // make sure to use real instance instead of proxy as context
+ return _createElement(this._self, tag, data, children)
+}
+
+function _createElement (
+ context,
+ tag,
+ data,
+ children
+) {
+ if (data && data.__ob__) {
+ "development" !== 'production' && warn(
+ "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + "\n" +
+ 'Always create fresh vnode data objects in each render!',
+ context
+ );
+ return
+ }
+ if (!tag) {
+ // in case of component :is set to falsy value
+ return emptyVNode()
+ }
+ if (typeof tag === 'string') {
+ var Ctor;
+ var ns = config.getTagNamespace(tag);
+ if (config.isReservedTag(tag)) {
+ // platform built-in elements
+ return new VNode(
+ tag, data, normalizeChildren(children, ns),
+ undefined, undefined, ns, context
+ )
+ } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
+ // component
+ return createComponent(Ctor, data, context, children, tag)
+ } else {
+ // unknown or unlisted namespaced elements
+ // check at runtime because it may get assigned a namespace when its
+ // parent normalizes children
+ return new VNode(
+ tag, data, normalizeChildren(children, ns),
+ undefined, undefined, ns, context
+ )
}
+ } else {
+ // direct component options / constructor
+ return createComponent(tag, data, context, children)
+ }
+}
+
+/* */
+
+function initRender (vm) {
+ vm.$vnode = null; // the placeholder node in parent tree
+ vm._vnode = null; // the root of the child tree
+ vm._staticTrees = null;
+ vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context;
+ vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext);
+ // bind the public createElement fn to this instance
+ // so that we get proper render context inside it.
+ vm.$createElement = bind$1(createElement, vm);
+ if (vm.$options.el) {
+ vm.$mount(vm.$options.el);
+ }
+}
+
+function renderMixin (Vue) {
+ Vue.prototype.$nextTick = function (fn) {
+ nextTick(fn, this);
+ };
- // a-z, A-Z
- if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) {
- return 'ident';
- }
+ Vue.prototype._render = function () {
+ var vm = this;
+ var ref = vm.$options;
+ var render = ref.render;
+ var staticRenderFns = ref.staticRenderFns;
+ var _parentVnode = ref._parentVnode;
- // 1-9
- if (code >= 0x31 && code <= 0x39) {
- return 'number';
+ if (vm._isMounted) {
+ // clone slot nodes on re-renders
+ for (var key in vm.$slots) {
+ vm.$slots[key] = cloneVNodes(vm.$slots[key]);
+ }
}
- return 'else';
- }
-
- /**
- * Format a subPath, return its plain form if it is
- * a literal string or number. Otherwise prepend the
- * dynamic indicator (*).
- *
- * @param {String} path
- * @return {String}
- */
-
- function formatSubPath(path) {
- var trimmed = path.trim();
- // invalid leading 0
- if (path.charAt(0) === '0' && isNaN(path)) {
- return false;
+ if (staticRenderFns && !vm._staticTrees) {
+ vm._staticTrees = [];
}
- return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed;
- }
-
- /**
- * Parse a string path into an array of segments
- *
- * @param {String} path
- * @return {Array|undefined}
- */
-
- function parse(path) {
- var keys = [];
- var index = -1;
- var mode = BEFORE_PATH;
- var subPathDepth = 0;
- var c, newChar, key, type, transition, action, typeMap;
-
- var actions = [];
-
- actions[PUSH] = function () {
- if (key !== undefined) {
- keys.push(key);
- key = undefined;
- }
- };
-
- actions[APPEND] = function () {
- if (key === undefined) {
- key = newChar;
- } else {
- key += newChar;
+ // set parent vnode. this allows render functions to have access
+ // to the data on the placeholder node.
+ vm.$vnode = _parentVnode;
+ // render self
+ var vnode;
+ try {
+ vnode = render.call(vm._renderProxy, vm.$createElement);
+ } catch (e) {
+ {
+ warn(("Error when rendering " + (formatComponentName(vm)) + ":"));
}
- };
-
- actions[INC_SUB_PATH_DEPTH] = function () {
- actions[APPEND]();
- subPathDepth++;
- };
-
- actions[PUSH_SUB_PATH] = function () {
- if (subPathDepth > 0) {
- subPathDepth--;
- mode = IN_SUB_PATH;
- actions[APPEND]();
+ /* istanbul ignore else */
+ if (config.errorHandler) {
+ config.errorHandler.call(null, e, vm);
} else {
- subPathDepth = 0;
- key = formatSubPath(key);
- if (key === false) {
- return false;
+ if (config._isServer) {
+ throw e
} else {
- actions[PUSH]();
+ setTimeout(function () { throw e }, 0);
}
}
- };
-
- function maybeUnescapeQuote() {
- var nextChar = path[index + 1];
- if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') {
- index++;
- newChar = '\\' + nextChar;
- actions[APPEND]();
- return true;
- }
+ // return previous vnode to prevent render error causing blank component
+ vnode = vm._vnode;
}
-
- while (mode != null) {
- index++;
- c = path[index];
-
- if (c === '\\' && maybeUnescapeQuote()) {
- continue;
- }
-
- type = getPathCharType(c);
- typeMap = pathStateMachine[mode];
- transition = typeMap[type] || typeMap['else'] || ERROR;
-
- if (transition === ERROR) {
- return; // parse error
+ // return empty vnode in case the render function errored out
+ if (!(vnode instanceof VNode)) {
+ if ("development" !== 'production' && Array.isArray(vnode)) {
+ warn(
+ 'Multiple root nodes returned from render function. Render function ' +
+ 'should return a single root node.',
+ vm
+ );
}
+ vnode = emptyVNode();
+ }
+ // set parent
+ vnode.parent = _parentVnode;
+ return vnode
+ };
- mode = transition[0];
- action = actions[transition[1]];
- if (action) {
- newChar = transition[2];
- newChar = newChar === undefined ? c : newChar;
- if (action() === false) {
- return;
+ // shorthands used in render functions
+ Vue.prototype._h = createElement;
+ // toString for mustaches
+ Vue.prototype._s = _toString;
+ // number conversion
+ Vue.prototype._n = toNumber;
+ // empty vnode
+ Vue.prototype._e = emptyVNode;
+ // loose equal
+ Vue.prototype._q = looseEqual;
+ // loose indexOf
+ Vue.prototype._i = looseIndexOf;
+
+ // render static tree by index
+ Vue.prototype._m = function renderStatic (
+ index,
+ isInFor
+ ) {
+ var tree = this._staticTrees[index];
+ // if has already-rendered static tree and not inside v-for,
+ // we can reuse the same tree by doing a shallow clone.
+ if (tree && !isInFor) {
+ return Array.isArray(tree)
+ ? cloneVNodes(tree)
+ : cloneVNode(tree)
+ }
+ // otherwise, render a fresh tree.
+ tree = this._staticTrees[index] = this.$options.staticRenderFns[index].call(this._renderProxy);
+ if (Array.isArray(tree)) {
+ for (var i = 0; i < tree.length; i++) {
+ if (typeof tree[i] !== 'string') {
+ tree[i].isStatic = true;
+ tree[i].key = "__static__" + index + "_" + i;
}
}
-
- if (mode === AFTER_PATH) {
- keys.raw = path;
- return keys;
- }
+ } else {
+ tree.isStatic = true;
+ tree.key = "__static__" + index;
}
- }
+ return tree
+ };
- /**
- * External parse that check for a cache hit first
- *
- * @param {String} path
- * @return {Array|undefined}
- */
+ // filter resolution helper
+ var identity = function (_) { return _; };
+ Vue.prototype._f = function resolveFilter (id) {
+ return resolveAsset(this.$options, 'filters', id, true) || identity
+ };
- function parsePath(path) {
- var hit = pathCache.get(path);
- if (!hit) {
- hit = parse(path);
- if (hit) {
- pathCache.put(path, hit);
+ // render v-for
+ Vue.prototype._l = function renderList (
+ val,
+ render
+ ) {
+ var ret, i, l, keys, key;
+ if (Array.isArray(val)) {
+ ret = new Array(val.length);
+ for (i = 0, l = val.length; i < l; i++) {
+ ret[i] = render(val[i], i);
+ }
+ } else if (typeof val === 'number') {
+ ret = new Array(val);
+ for (i = 0; i < val; i++) {
+ ret[i] = render(i + 1, i);
+ }
+ } else if (isObject(val)) {
+ keys = Object.keys(val);
+ ret = new Array(keys.length);
+ for (i = 0, l = keys.length; i < l; i++) {
+ key = keys[i];
+ ret[i] = render(val[key], key, i);
}
}
- return hit;
- }
-
- /**
- * Get from an object from a path string
- *
- * @param {Object} obj
- * @param {String} path
- */
-
- function getPath(obj, path) {
- return parseExpression(path).get(obj);
- }
-
- /**
- * Warn against setting non-existent root path on a vm.
- */
-
- var warnNonExistent;
- if ('development' !== 'production') {
- warnNonExistent = function (path, vm) {
- warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm);
- };
- }
+ return ret
+ };
- /**
- * Set on an object from a path
- *
- * @param {Object} obj
- * @param {String | Array} path
- * @param {*} val
- */
+ // renderSlot
+ Vue.prototype._t = function (
+ name,
+ fallback
+ ) {
+ var slotNodes = this.$slots[name];
+ // warn duplicate slot usage
+ if (slotNodes && "development" !== 'production') {
+ slotNodes._rendered && warn(
+ "Duplicate presence of slot \"" + name + "\" found in the same render tree " +
+ "- this will likely cause render errors.",
+ this
+ );
+ slotNodes._rendered = true;
+ }
+ return slotNodes || fallback
+ };
- function setPath(obj, path, val) {
- var original = obj;
- if (typeof path === 'string') {
- path = parse(path);
- }
- if (!path || !isObject(obj)) {
- return false;
- }
- var last, key;
- for (var i = 0, l = path.length; i < l; i++) {
- last = obj;
- key = path[i];
- if (key.charAt(0) === '*') {
- key = parseExpression(key.slice(1)).get.call(original, original);
- }
- if (i < l - 1) {
- obj = obj[key];
- if (!isObject(obj)) {
- obj = {};
- if ('development' !== 'production' && last._isVue) {
- warnNonExistent(path, last);
- }
- set(last, key, obj);
- }
+ // apply v-bind object
+ Vue.prototype._b = function bindProps (
+ data,
+ value,
+ asProp
+ ) {
+ if (value) {
+ if (!isObject(value)) {
+ "development" !== 'production' && warn(
+ 'v-bind without argument expects an Object or Array value',
+ this
+ );
} else {
- if (isArray(obj)) {
- obj.$set(key, val);
- } else if (key in obj) {
- obj[key] = val;
- } else {
- if ('development' !== 'production' && obj._isVue) {
- warnNonExistent(path, obj);
+ if (Array.isArray(value)) {
+ value = toObject(value);
+ }
+ for (var key in value) {
+ if (key === 'class' || key === 'style') {
+ data[key] = value[key];
+ } else {
+ var hash = asProp || config.mustUseProp(key)
+ ? data.domProps || (data.domProps = {})
+ : data.attrs || (data.attrs = {});
+ hash[key] = value[key];
}
- set(obj, key, val);
}
}
}
- return true;
- }
-
-var path = Object.freeze({
- parsePath: parsePath,
- getPath: getPath,
- setPath: setPath
- });
-
- var expressionCache = new Cache(1000);
-
- var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat';
- var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)');
-
- // keywords that don't make sense inside expressions
- var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public';
- var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)');
-
- var wsRE = /\s/g;
- var newlineRE = /\n/g;
- var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g;
- var restoreRE = /"(\d+)"/g;
- var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
- var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g;
- var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/;
-
- function noop() {}
-
- /**
- * Save / Rewrite / Restore
- *
- * When rewriting paths found in an expression, it is
- * possible for the same letter sequences to be found in
- * strings and Object literal property keys. Therefore we
- * remove and store these parts in a temporary array, and
- * restore them after the path rewrite.
- */
-
- var saved = [];
-
- /**
- * Save replacer
- *
- * The save regex can match two possible cases:
- * 1. An opening object literal
- * 2. A string
- * If matched as a plain string, we need to escape its
- * newlines, since the string needs to be preserved when
- * generating the function body.
- *
- * @param {String} str
- * @param {String} isString - str if matched as a string
- * @return {String} - placeholder with index
- */
-
- function save(str, isString) {
- var i = saved.length;
- saved[i] = isString ? str.replace(newlineRE, '\\n') : str;
- return '"' + i + '"';
- }
-
- /**
- * Path rewrite replacer
- *
- * @param {String} raw
- * @return {String}
- */
+ return data
+ };
- function rewrite(raw) {
- var c = raw.charAt(0);
- var path = raw.slice(1);
- if (allowedKeywordsRE.test(path)) {
- return raw;
+ // expose v-on keyCodes
+ Vue.prototype._k = function getKeyCodes (key) {
+ return config.keyCodes[key]
+ };
+}
+
+function resolveSlots (
+ renderChildren,
+ context
+) {
+ var slots = {};
+ if (!renderChildren) {
+ return slots
+ }
+ var children = normalizeChildren(renderChildren) || [];
+ var defaultSlot = [];
+ var name, child;
+ for (var i = 0, l = children.length; i < l; i++) {
+ child = children[i];
+ // named slots should only be respected if the vnode was rendered in the
+ // same context.
+ if ((child.context === context || child.functionalContext === context) &&
+ child.data && (name = child.data.slot)) {
+ var slot = (slots[name] || (slots[name] = []));
+ if (child.tag === 'template') {
+ slot.push.apply(slot, child.children);
+ } else {
+ slot.push(child);
+ }
} else {
- path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path;
- return c + 'scope.' + path;
+ defaultSlot.push(child);
}
}
-
- /**
- * Restore replacer
- *
- * @param {String} str
- * @param {String} i - matched save index
- * @return {String}
- */
-
- function restore(str, i) {
- return saved[i];
+ // ignore single whitespace
+ if (defaultSlot.length && !(
+ defaultSlot.length === 1 &&
+ (defaultSlot[0].text === ' ' || defaultSlot[0].isComment)
+ )) {
+ slots.default = defaultSlot;
}
+ return slots
+}
- /**
- * Rewrite an expression, prefixing all path accessors with
- * `scope.` and generate getter/setter functions.
- *
- * @param {String} exp
- * @return {Function}
- */
+/* */
- function compileGetter(exp) {
- if (improperKeywordsRE.test(exp)) {
- 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp);
- }
- // reset state
- saved.length = 0;
- // save strings and object literal keys
- var body = exp.replace(saveRE, save).replace(wsRE, '');
- // rewrite all paths
- // pad 1 space here because the regex matches 1 extra char
- body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore);
- return makeGetterFn(body);
+function initEvents (vm) {
+ vm._events = Object.create(null);
+ // init parent attached events
+ var listeners = vm.$options._parentListeners;
+ var on = bind$1(vm.$on, vm);
+ var off = bind$1(vm.$off, vm);
+ vm._updateListeners = function (listeners, oldListeners) {
+ updateListeners(listeners, oldListeners || {}, on, off, vm);
+ };
+ if (listeners) {
+ vm._updateListeners(listeners);
}
+}
- /**
- * Build a getter function. Requires eval.
- *
- * We isolate the try/catch so it doesn't affect the
- * optimization of the parse function when it is not called.
- *
- * @param {String} body
- * @return {Function|undefined}
- */
+function eventsMixin (Vue) {
+ Vue.prototype.$on = function (event, fn) {
+ var vm = this;(vm._events[event] || (vm._events[event] = [])).push(fn);
+ return vm
+ };
- function makeGetterFn(body) {
- try {
- /* eslint-disable no-new-func */
- return new Function('scope', 'return ' + body + ';');
- /* eslint-enable no-new-func */
- } catch (e) {
- if ('development' !== 'production') {
- /* istanbul ignore if */
- if (e.toString().match(/unsafe-eval|CSP/)) {
- warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build');
- } else {
- warn('Invalid expression. ' + 'Generated function body: ' + body);
- }
- }
- return noop;
+ Vue.prototype.$once = function (event, fn) {
+ var vm = this;
+ function on () {
+ vm.$off(event, on);
+ fn.apply(vm, arguments);
}
- }
-
- /**
- * Compile a setter function for the expression.
- *
- * @param {String} exp
- * @return {Function|undefined}
- */
+ on.fn = fn;
+ vm.$on(event, on);
+ return vm
+ };
- function compileSetter(exp) {
- var path = parsePath(exp);
- if (path) {
- return function (scope, val) {
- setPath(scope, path, val);
- };
- } else {
- 'development' !== 'production' && warn('Invalid setter expression: ' + exp);
+ Vue.prototype.$off = function (event, fn) {
+ var vm = this;
+ // all
+ if (!arguments.length) {
+ vm._events = Object.create(null);
+ return vm
+ }
+ // specific event
+ var cbs = vm._events[event];
+ if (!cbs) {
+ return vm
+ }
+ if (arguments.length === 1) {
+ vm._events[event] = null;
+ return vm
+ }
+ // specific handler
+ var cb;
+ var i = cbs.length;
+ while (i--) {
+ cb = cbs[i];
+ if (cb === fn || cb.fn === fn) {
+ cbs.splice(i, 1);
+ break
+ }
}
- }
-
- /**
- * Parse an expression into re-written getter/setters.
- *
- * @param {String} exp
- * @param {Boolean} needSet
- * @return {Function}
- */
+ return vm
+ };
- function parseExpression(exp, needSet) {
- exp = exp.trim();
- // try cache
- var hit = expressionCache.get(exp);
- if (hit) {
- if (needSet && !hit.set) {
- hit.set = compileSetter(hit.exp);
+ Vue.prototype.$emit = function (event) {
+ var vm = this;
+ var cbs = vm._events[event];
+ if (cbs) {
+ cbs = cbs.length > 1 ? toArray(cbs) : cbs;
+ var args = toArray(arguments, 1);
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ cbs[i].apply(vm, args);
}
- return hit;
}
- var res = { exp: exp };
- res.get = isSimplePath(exp) && exp.indexOf('[') < 0
- // optimized super simple getter
- ? makeGetterFn('scope.' + exp)
- // dynamic getter
- : compileGetter(exp);
- if (needSet) {
- res.set = compileSetter(exp);
+ return vm
+ };
+}
+
+/* */
+
+var uid = 0;
+
+function initMixin (Vue) {
+ Vue.prototype._init = function (options) {
+ var vm = this;
+ // a uid
+ vm._uid = uid++;
+ // a flag to avoid this being observed
+ vm._isVue = true;
+ // merge options
+ if (options && options._isComponent) {
+ // optimize internal component instantiation
+ // since dynamic options merging is pretty slow, and none of the
+ // internal component options needs special treatment.
+ initInternalComponent(vm, options);
+ } else {
+ vm.$options = mergeOptions(
+ resolveConstructorOptions(vm),
+ options || {},
+ vm
+ );
}
- expressionCache.put(exp, res);
- return res;
- }
-
- /**
- * Check if an expression is a simple path.
- *
- * @param {String} exp
- * @return {Boolean}
- */
-
- function isSimplePath(exp) {
- return pathTestRE.test(exp) &&
- // don't treat literal values as paths
- !literalValueRE$1.test(exp) &&
- // Math constants e.g. Math.PI, Math.E etc.
- exp.slice(0, 5) !== 'Math.';
- }
-
-var expression = Object.freeze({
- parseExpression: parseExpression,
- isSimplePath: isSimplePath
- });
-
- // we have two separate queues: one for directive updates
- // and one for user watcher registered via $watch().
- // we want to guarantee directive updates to be called
- // before user watchers so that when user watchers are
- // triggered, the DOM would have already been in updated
- // state.
-
- var queue = [];
- var userQueue = [];
- var has = {};
- var circular = {};
- var waiting = false;
-
- /**
- * Reset the batcher's state.
- */
-
- function resetBatcherState() {
- queue.length = 0;
- userQueue.length = 0;
- has = {};
- circular = {};
- waiting = false;
- }
-
- /**
- * Flush both queues and run the watchers.
- */
-
- function flushBatcherQueue() {
- var _again = true;
-
- _function: while (_again) {
- _again = false;
+ /* istanbul ignore else */
+ {
+ initProxy(vm);
+ }
+ // expose real self
+ vm._self = vm;
+ initLifecycle(vm);
+ initEvents(vm);
+ callHook(vm, 'beforeCreate');
+ initState(vm);
+ callHook(vm, 'created');
+ initRender(vm);
+ };
- runBatcherQueue(queue);
- runBatcherQueue(userQueue);
- // user watchers triggered more watchers,
- // keep flushing until it depletes
- if (queue.length) {
- _again = true;
- continue _function;
- }
- // dev tool hook
- /* istanbul ignore if */
- if (devtools && config.devtools) {
- devtools.emit('flush');
- }
- resetBatcherState();
+ function initInternalComponent (vm, options) {
+ var opts = vm.$options = Object.create(resolveConstructorOptions(vm));
+ // doing this because it's faster than dynamic enumeration.
+ opts.parent = options.parent;
+ opts.propsData = options.propsData;
+ opts._parentVnode = options._parentVnode;
+ opts._parentListeners = options._parentListeners;
+ opts._renderChildren = options._renderChildren;
+ opts._componentTag = options._componentTag;
+ if (options.render) {
+ opts.render = options.render;
+ opts.staticRenderFns = options.staticRenderFns;
}
}
- /**
- * Run the watchers in a single queue.
- *
- * @param {Array} queue
- */
-
- function runBatcherQueue(queue) {
- // do not cache length because more watchers might be pushed
- // as we run existing watchers
- for (var i = 0; i < queue.length; i++) {
- var watcher = queue[i];
- var id = watcher.id;
- has[id] = null;
- watcher.run();
- // in dev build, check and stop circular updates.
- if ('development' !== 'production' && has[id] != null) {
- circular[id] = (circular[id] || 0) + 1;
- if (circular[id] > config._maxUpdateCount) {
- warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm);
- break;
+ function resolveConstructorOptions (vm) {
+ var Ctor = vm.constructor;
+ var options = Ctor.options;
+ if (Ctor.super) {
+ var superOptions = Ctor.super.options;
+ var cachedSuperOptions = Ctor.superOptions;
+ if (superOptions !== cachedSuperOptions) {
+ // super option changed
+ Ctor.superOptions = superOptions;
+ options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
+ if (options.name) {
+ options.components[options.name] = Ctor;
}
}
}
- queue.length = 0;
+ return options
}
+}
- /**
- * Push a watcher into the watcher queue.
- * Jobs with duplicate IDs will be skipped unless it's
- * pushed when the queue is being flushed.
- *
- * @param {Watcher} watcher
- * properties:
- * - {Number} id
- * - {Function} run
- */
-
- function pushWatcher(watcher) {
- var id = watcher.id;
- if (has[id] == null) {
- // push watcher into appropriate queue
- var q = watcher.user ? userQueue : queue;
- has[id] = q.length;
- q.push(watcher);
- // queue the flush
- if (!waiting) {
- waiting = true;
- nextTick(flushBatcherQueue);
- }
- }
- }
-
- var uid$2 = 0;
-
- /**
- * A watcher parses an expression, collects dependencies,
- * and fires callback when the expression value changes.
- * This is used for both the $watch() api and directives.
- *
- * @param {Vue} vm
- * @param {String|Function} expOrFn
- * @param {Function} cb
- * @param {Object} options
- * - {Array} filters
- * - {Boolean} twoWay
- * - {Boolean} deep
- * - {Boolean} user
- * - {Boolean} sync
- * - {Boolean} lazy
- * - {Function} [preProcess]
- * - {Function} [postProcess]
- * @constructor
- */
- function Watcher(vm, expOrFn, cb, options) {
- // mix in options
- if (options) {
- extend(this, options);
- }
- var isFn = typeof expOrFn === 'function';
- this.vm = vm;
- vm._watchers.push(this);
- this.expression = expOrFn;
- this.cb = cb;
- this.id = ++uid$2; // uid for batching
- this.active = true;
- this.dirty = this.lazy; // for lazy watchers
- this.deps = [];
- this.newDeps = [];
- this.depIds = new _Set();
- this.newDepIds = new _Set();
- this.prevError = null; // for async error stacks
- // parse expression for getter/setter
- if (isFn) {
- this.getter = expOrFn;
- this.setter = undefined;
- } else {
- var res = parseExpression(expOrFn, this.twoWay);
- this.getter = res.get;
- this.setter = res.set;
- }
- this.value = this.lazy ? undefined : this.get();
- // state for avoiding false triggers for deep and Array
- // watchers during vm._digest()
- this.queued = this.shallow = false;
+function Vue$3 (options) {
+ if ("development" !== 'production' &&
+ !(this instanceof Vue$3)) {
+ warn('Vue is a constructor and should be called with the `new` keyword');
}
+ this._init(options);
+}
- /**
- * Evaluate the getter, and re-collect dependencies.
- */
+initMixin(Vue$3);
+stateMixin(Vue$3);
+eventsMixin(Vue$3);
+lifecycleMixin(Vue$3);
+renderMixin(Vue$3);
- Watcher.prototype.get = function () {
- this.beforeGet();
- var scope = this.scope || this.vm;
- var value;
- try {
- value = this.getter.call(scope, scope);
- } catch (e) {
- if ('development' !== 'production' && config.warnExpressionErrors) {
- warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm);
- }
- }
- // "touch" every property so they are all tracked as
- // dependencies for deep watching
- if (this.deep) {
- traverse(value);
- }
- if (this.preProcess) {
- value = this.preProcess(value);
- }
- if (this.filters) {
- value = scope._applyFilters(value, null, this.filters, false);
- }
- if (this.postProcess) {
- value = this.postProcess(value);
- }
- this.afterGet();
- return value;
- };
+var warn = noop;
+var formatComponentName;
- /**
- * Set the corresponding value with the setter.
- *
- * @param {*} value
- */
+{
+ var hasConsole = typeof console !== 'undefined';
- Watcher.prototype.set = function (value) {
- var scope = this.scope || this.vm;
- if (this.filters) {
- value = scope._applyFilters(value, this.value, this.filters, true);
- }
- try {
- this.setter.call(scope, scope, value);
- } catch (e) {
- if ('development' !== 'production' && config.warnExpressionErrors) {
- warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm);
- }
- }
- // two-way sync for v-for alias
- var forContext = scope.$forContext;
- if (forContext && forContext.alias === this.expression) {
- if (forContext.filters) {
- 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm);
- return;
- }
- forContext._withLock(function () {
- if (scope.$key) {
- // original is an object
- forContext.rawValue[scope.$key] = value;
- } else {
- forContext.rawValue.$set(scope.$index, value);
- }
- });
+ warn = function (msg, vm) {
+ if (hasConsole && (!config.silent)) {
+ console.error("[Vue warn]: " + msg + " " + (
+ vm ? formatLocation(formatComponentName(vm)) : ''
+ ));
}
};
- /**
- * Prepare for dependency collection.
- */
-
- Watcher.prototype.beforeGet = function () {
- Dep.target = this;
+ formatComponentName = function (vm) {
+ if (vm.$root === vm) {
+ return 'root instance'
+ }
+ var name = vm._isVue
+ ? vm.$options.name || vm.$options._componentTag
+ : vm.name;
+ return (
+ (name ? ("component <" + name + ">") : "anonymous component") +
+ (vm._isVue && vm.$options.__file ? (" at " + (vm.$options.__file)) : '')
+ )
};
- /**
- * Add a dependency to this directive.
- *
- * @param {Dep} dep
- */
-
- Watcher.prototype.addDep = function (dep) {
- var id = dep.id;
- if (!this.newDepIds.has(id)) {
- this.newDepIds.add(id);
- this.newDeps.push(dep);
- if (!this.depIds.has(id)) {
- dep.addSub(this);
- }
+ var formatLocation = function (str) {
+ if (str === 'anonymous component') {
+ str += " - use the \"name\" option for better debugging messages.";
}
+ return ("\n(found in " + str + ")")
};
+}
- /**
- * Clean up for dependency collection.
- */
-
- Watcher.prototype.afterGet = function () {
- Dep.target = null;
- var i = this.deps.length;
- while (i--) {
- var dep = this.deps[i];
- if (!this.newDepIds.has(dep.id)) {
- dep.removeSub(this);
- }
- }
- var tmp = this.depIds;
- this.depIds = this.newDepIds;
- this.newDepIds = tmp;
- this.newDepIds.clear();
- tmp = this.deps;
- this.deps = this.newDeps;
- this.newDeps = tmp;
- this.newDeps.length = 0;
- };
+/* */
- /**
- * Subscriber interface.
- * Will be called when a dependency changes.
- *
- * @param {Boolean} shallow
- */
+/**
+ * Option overwriting strategies are functions that handle
+ * how to merge a parent option value and a child option
+ * value into the final value.
+ */
+var strats = config.optionMergeStrategies;
- Watcher.prototype.update = function (shallow) {
- if (this.lazy) {
- this.dirty = true;
- } else if (this.sync || !config.async) {
- this.run();
- } else {
- // if queued, only overwrite shallow with non-shallow,
- // but not the other way around.
- this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow;
- this.queued = true;
- // record before-push error stack in debug mode
- /* istanbul ignore if */
- if ('development' !== 'production' && config.debug) {
- this.prevError = new Error('[vue] async stack trace');
- }
- pushWatcher(this);
+/**
+ * Options with restrictions
+ */
+{
+ strats.el = strats.propsData = function (parent, child, vm, key) {
+ if (!vm) {
+ warn(
+ "option \"" + key + "\" can only be used during instance " +
+ 'creation with the `new` keyword.'
+ );
}
+ return defaultStrat(parent, child)
};
+}
- /**
- * Batcher job interface.
- * Will be called by the batcher.
- */
-
- Watcher.prototype.run = function () {
- if (this.active) {
- var value = this.get();
- if (value !== this.value ||
- // Deep watchers and watchers on Object/Arrays should fire even
- // when the value is the same, because the value may
- // have mutated; but only do so if this is a
- // non-shallow update (caused by a vm digest).
- (isObject(value) || this.deep) && !this.shallow) {
- // set new value
- var oldValue = this.value;
- this.value = value;
- // in debug + async mode, when a watcher callbacks
- // throws, we also throw the saved before-push error
- // so the full cross-tick stack trace is available.
- var prevError = this.prevError;
- /* istanbul ignore if */
- if ('development' !== 'production' && config.debug && prevError) {
- this.prevError = null;
- try {
- this.cb.call(this.vm, value, oldValue);
- } catch (e) {
- nextTick(function () {
- throw prevError;
- }, 0);
- throw e;
- }
- } else {
- this.cb.call(this.vm, value, oldValue);
- }
+/**
+ * Helper that recursively merges two data objects together.
+ */
+function mergeData (to, from) {
+ var key, toVal, fromVal;
+ for (key in from) {
+ toVal = to[key];
+ fromVal = from[key];
+ if (!hasOwn(to, key)) {
+ set(to, key, fromVal);
+ } else if (isObject(toVal) && isObject(fromVal)) {
+ mergeData(toVal, fromVal);
+ }
+ }
+ return to
+}
+
+/**
+ * Data
+ */
+strats.data = function (
+ parentVal,
+ childVal,
+ vm
+) {
+ if (!vm) {
+ // in a Vue.extend merge, both should be functions
+ if (!childVal) {
+ return parentVal
+ }
+ if (typeof childVal !== 'function') {
+ "development" !== 'production' && warn(
+ 'The "data" option should be a function ' +
+ 'that returns a per-instance value in component ' +
+ 'definitions.',
+ vm
+ );
+ return parentVal
+ }
+ if (!parentVal) {
+ return childVal
+ }
+ // when parentVal & childVal are both present,
+ // we need to return a function that returns the
+ // merged result of both functions... no need to
+ // check if parentVal is a function here because
+ // it has to be a function to pass previous merges.
+ return function mergedDataFn () {
+ return mergeData(
+ childVal.call(this),
+ parentVal.call(this)
+ )
+ }
+ } else if (parentVal || childVal) {
+ return function mergedInstanceDataFn () {
+ // instance merge
+ var instanceData = typeof childVal === 'function'
+ ? childVal.call(vm)
+ : childVal;
+ var defaultData = typeof parentVal === 'function'
+ ? parentVal.call(vm)
+ : undefined;
+ if (instanceData) {
+ return mergeData(instanceData, defaultData)
+ } else {
+ return defaultData
}
- this.queued = this.shallow = false;
}
- };
-
- /**
- * Evaluate the value of the watcher.
- * This only gets called for lazy watchers.
- */
-
- Watcher.prototype.evaluate = function () {
- // avoid overwriting another watcher that is being
- // collected.
- var current = Dep.target;
- this.value = this.get();
- this.dirty = false;
- Dep.target = current;
- };
-
- /**
- * Depend on all deps collected by this watcher.
- */
-
- Watcher.prototype.depend = function () {
- var i = this.deps.length;
- while (i--) {
- this.deps[i].depend();
- }
- };
-
- /**
- * Remove self from all dependencies' subcriber list.
- */
+ }
+};
- Watcher.prototype.teardown = function () {
- if (this.active) {
- // remove self from vm's watcher list
- // this is a somewhat expensive operation so we skip it
- // if the vm is being destroyed or is performing a v-for
- // re-render (the watcher list is then filtered by v-for).
- if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
- this.vm._watchers.$remove(this);
+/**
+ * Hooks and param attributes are merged as arrays.
+ */
+function mergeHook (
+ parentVal,
+ childVal
+) {
+ return childVal
+ ? parentVal
+ ? parentVal.concat(childVal)
+ : Array.isArray(childVal)
+ ? childVal
+ : [childVal]
+ : parentVal
+}
+
+config._lifecycleHooks.forEach(function (hook) {
+ strats[hook] = mergeHook;
+});
+
+/**
+ * Assets
+ *
+ * When a vm is present (instance creation), we need to do
+ * a three-way merge between constructor options, instance
+ * options and parent options.
+ */
+function mergeAssets (parentVal, childVal) {
+ var res = Object.create(parentVal || null);
+ return childVal
+ ? extend(res, childVal)
+ : res
+}
+
+config._assetTypes.forEach(function (type) {
+ strats[type + 's'] = mergeAssets;
+});
+
+/**
+ * Watchers.
+ *
+ * Watchers hashes should not overwrite one
+ * another, so we merge them as arrays.
+ */
+strats.watch = function (parentVal, childVal) {
+ /* istanbul ignore if */
+ if (!childVal) { return parentVal }
+ if (!parentVal) { return childVal }
+ var ret = {};
+ extend(ret, parentVal);
+ for (var key in childVal) {
+ var parent = ret[key];
+ var child = childVal[key];
+ if (parent && !Array.isArray(parent)) {
+ parent = [parent];
+ }
+ ret[key] = parent
+ ? parent.concat(child)
+ : [child];
+ }
+ return ret
+};
+
+/**
+ * Other object hashes.
+ */
+strats.props =
+strats.methods =
+strats.computed = function (parentVal, childVal) {
+ if (!childVal) { return parentVal }
+ if (!parentVal) { return childVal }
+ var ret = Object.create(null);
+ extend(ret, parentVal);
+ extend(ret, childVal);
+ return ret
+};
+
+/**
+ * Default strategy.
+ */
+var defaultStrat = function (parentVal, childVal) {
+ return childVal === undefined
+ ? parentVal
+ : childVal
+};
+
+/**
+ * Make sure component options get converted to actual
+ * constructors.
+ */
+function normalizeComponents (options) {
+ if (options.components) {
+ var components = options.components;
+ var def;
+ for (var key in components) {
+ var lower = key.toLowerCase();
+ if (isBuiltInTag(lower) || config.isReservedTag(lower)) {
+ "development" !== 'production' && warn(
+ 'Do not use built-in or reserved HTML elements as component ' +
+ 'id: ' + key
+ );
+ continue
}
- var i = this.deps.length;
- while (i--) {
- this.deps[i].removeSub(this);
+ def = components[key];
+ if (isPlainObject(def)) {
+ components[key] = Vue$3.extend(def);
}
- this.active = false;
- this.vm = this.cb = this.value = null;
}
- };
-
- /**
- * Recrusively traverse an object to evoke all converted
- * getters, so that every nested property inside the object
- * is collected as a "deep" dependency.
- *
- * @param {*} val
- */
+ }
+}
- var seenObjects = new _Set();
- function traverse(val, seen) {
- var i = undefined,
- keys = undefined;
- if (!seen) {
- seen = seenObjects;
- seen.clear();
- }
- var isA = isArray(val);
- var isO = isObject(val);
- if ((isA || isO) && Object.isExtensible(val)) {
- if (val.__ob__) {
- var depId = val.__ob__.dep.id;
- if (seen.has(depId)) {
- return;
- } else {
- seen.add(depId);
- }
- }
- if (isA) {
- i = val.length;
- while (i--) traverse(val[i], seen);
- } else if (isO) {
- keys = Object.keys(val);
- i = keys.length;
- while (i--) traverse(val[keys[i]], seen);
+/**
+ * Ensure all props option syntax are normalized into the
+ * Object-based format.
+ */
+function normalizeProps (options) {
+ var props = options.props;
+ if (!props) { return }
+ var res = {};
+ var i, val, name;
+ if (Array.isArray(props)) {
+ i = props.length;
+ while (i--) {
+ val = props[i];
+ if (typeof val === 'string') {
+ name = camelize(val);
+ res[name] = { type: null };
+ } else {
+ warn('props must be strings when using array syntax.');
}
}
- }
-
- var text$1 = {
-
- bind: function bind() {
- this.attr = this.el.nodeType === 3 ? 'data' : 'textContent';
- },
-
- update: function update(value) {
- this.el[this.attr] = _toString(value);
+ } else if (isPlainObject(props)) {
+ for (var key in props) {
+ val = props[key];
+ name = camelize(key);
+ res[name] = isPlainObject(val)
+ ? val
+ : { type: val };
}
- };
-
- var templateCache = new Cache(1000);
- var idSelectorCache = new Cache(1000);
-
- var map = {
- efault: [0, '', ''],
- legend: [1, '<fieldset>', '</fieldset>'],
- tr: [2, '<table><tbody>', '</tbody></table>'],
- col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>']
- };
-
- map.td = map.th = [3, '<table><tbody><tr>', '</tr></tbody></table>'];
-
- map.option = map.optgroup = [1, '<select multiple="multiple">', '</select>'];
-
- map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '<table>', '</table>'];
-
- map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '<svg ' + 'xmlns="http://www.w3.org/2000/svg" ' + 'xmlns:xlink="http://www.w3.org/1999/xlink" ' + 'xmlns:ev="http://www.w3.org/2001/xml-events"' + 'version="1.1">', '</svg>'];
-
- /**
- * Check if a node is a supported template node with a
- * DocumentFragment content.
- *
- * @param {Node} node
- * @return {Boolean}
- */
-
- function isRealTemplate(node) {
- return isTemplate(node) && isFragment(node.content);
}
+ options.props = res;
+}
- var tagRE$1 = /<([\w:-]+)/;
- var entityRE = /&#?\w+?;/;
- var commentRE = /<!--/;
-
- /**
- * Convert a string template to a DocumentFragment.
- * Determines correct wrapping by tag types. Wrapping
- * strategy found in jQuery & component/domify.
- *
- * @param {String} templateString
- * @param {Boolean} raw
- * @return {DocumentFragment}
- */
-
- function stringToFragment(templateString, raw) {
- // try a cache hit first
- var cacheKey = raw ? templateString : templateString.trim();
- var hit = templateCache.get(cacheKey);
- if (hit) {
- return hit;
- }
-
- var frag = document.createDocumentFragment();
- var tagMatch = templateString.match(tagRE$1);
- var entityMatch = entityRE.test(templateString);
- var commentMatch = commentRE.test(templateString);
-
- if (!tagMatch && !entityMatch && !commentMatch) {
- // text only, return a single text node.
- frag.appendChild(document.createTextNode(templateString));
- } else {
- var tag = tagMatch && tagMatch[1];
- var wrap = map[tag] || map.efault;
- var depth = wrap[0];
- var prefix = wrap[1];
- var suffix = wrap[2];
- var node = document.createElement('div');
-
- node.innerHTML = prefix + templateString + suffix;
- while (depth--) {
- node = node.lastChild;
- }
-
- var child;
- /* eslint-disable no-cond-assign */
- while (child = node.firstChild) {
- /* eslint-enable no-cond-assign */
- frag.appendChild(child);
+/**
+ * Normalize raw function directives into object format.
+ */
+function normalizeDirectives (options) {
+ var dirs = options.directives;
+ if (dirs) {
+ for (var key in dirs) {
+ var def = dirs[key];
+ if (typeof def === 'function') {
+ dirs[key] = { bind: def, update: def };
}
}
- if (!raw) {
- trimNode(frag);
- }
- templateCache.put(cacheKey, frag);
- return frag;
}
+}
- /**
- * Convert a template node to a DocumentFragment.
- *
- * @param {Node} node
- * @return {DocumentFragment}
- */
-
- function nodeToFragment(node) {
- // if its a template tag and the browser supports it,
- // its content is already a document fragment. However, iOS Safari has
- // bug when using directly cloned template content with touch
- // events and can cause crashes when the nodes are removed from DOM, so we
- // have to treat template elements as string templates. (#2805)
- /* istanbul ignore if */
- if (isRealTemplate(node)) {
- return stringToFragment(node.innerHTML);
- }
- // script template
- if (node.tagName === 'SCRIPT') {
- return stringToFragment(node.textContent);
- }
- // normal node, clone it to avoid mutating the original
- var clonedNode = cloneNode(node);
- var frag = document.createDocumentFragment();
- var child;
- /* eslint-disable no-cond-assign */
- while (child = clonedNode.firstChild) {
- /* eslint-enable no-cond-assign */
- frag.appendChild(child);
+/**
+ * Merge two option objects into a new one.
+ * Core utility used in both instantiation and inheritance.
+ */
+function mergeOptions (
+ parent,
+ child,
+ vm
+) {
+ normalizeComponents(child);
+ normalizeProps(child);
+ normalizeDirectives(child);
+ var extendsFrom = child.extends;
+ if (extendsFrom) {
+ parent = typeof extendsFrom === 'function'
+ ? mergeOptions(parent, extendsFrom.options, vm)
+ : mergeOptions(parent, extendsFrom, vm);
+ }
+ if (child.mixins) {
+ for (var i = 0, l = child.mixins.length; i < l; i++) {
+ var mixin = child.mixins[i];
+ if (mixin.prototype instanceof Vue$3) {
+ mixin = mixin.options;
+ }
+ parent = mergeOptions(parent, mixin, vm);
+ }
+ }
+ var options = {};
+ var key;
+ for (key in parent) {
+ mergeField(key);
+ }
+ for (key in child) {
+ if (!hasOwn(parent, key)) {
+ mergeField(key);
}
- trimNode(frag);
- return frag;
}
-
- // Test for the presence of the Safari template cloning bug
- // https://bugs.webkit.org/showug.cgi?id=137755
- var hasBrokenTemplate = (function () {
- /* istanbul ignore else */
- if (inBrowser) {
- var a = document.createElement('div');
- a.innerHTML = '<template>1</template>';
- return !a.cloneNode(true).firstChild.innerHTML;
- } else {
- return false;
- }
- })();
-
- // Test for IE10/11 textarea placeholder clone bug
- var hasTextareaCloneBug = (function () {
- /* istanbul ignore else */
- if (inBrowser) {
- var t = document.createElement('textarea');
- t.placeholder = 't';
- return t.cloneNode(true).value === 't';
- } else {
- return false;
- }
- })();
-
- /**
- * 1. Deal with Safari cloning nested <template> bug by
- * manually cloning all template instances.
- * 2. Deal with IE10/11 textarea placeholder bug by setting
- * the correct value after cloning.
- *
- * @param {Element|DocumentFragment} node
- * @return {Element|DocumentFragment}
- */
-
- function cloneNode(node) {
- /* istanbul ignore if */
- if (!node.querySelectorAll) {
- return node.cloneNode();
- }
- var res = node.cloneNode(true);
- var i, original, cloned;
- /* istanbul ignore if */
- if (hasBrokenTemplate) {
- var tempClone = res;
- if (isRealTemplate(node)) {
- node = node.content;
- tempClone = res.content;
- }
- original = node.querySelectorAll('template');
- if (original.length) {
- cloned = tempClone.querySelectorAll('template');
- i = cloned.length;
- while (i--) {
- cloned[i].parentNode.replaceChild(cloneNode(original[i]), cloned[i]);
- }
- }
- }
- /* istanbul ignore if */
- if (hasTextareaCloneBug) {
- if (node.tagName === 'TEXTAREA') {
- res.value = node.value;
- } else {
- original = node.querySelectorAll('textarea');
- if (original.length) {
- cloned = res.querySelectorAll('textarea');
- i = cloned.length;
- while (i--) {
- cloned[i].value = original[i].value;
- }
- }
- }
- }
- return res;
+ function mergeField (key) {
+ var strat = strats[key] || defaultStrat;
+ options[key] = strat(parent[key], child[key], vm, key);
}
+ return options
+}
- /**
- * Process the template option and normalizes it into a
- * a DocumentFragment that can be used as a partial or a
- * instance template.
- *
- * @param {*} template
- * Possible values include:
- * - DocumentFragment object
- * - Node object of type Template
- * - id selector: '#some-template-id'
- * - template string: '<div><span>{{msg}}</span></div>'
- * @param {Boolean} shouldClone
- * @param {Boolean} raw
- * inline HTML interpolation. Do not check for id
- * selector and keep whitespace in the string.
- * @return {DocumentFragment|undefined}
- */
-
- function parseTemplate(template, shouldClone, raw) {
- var node, frag;
-
- // if the template is already a document fragment,
- // do nothing
- if (isFragment(template)) {
- trimNode(template);
- return shouldClone ? cloneNode(template) : template;
- }
-
- if (typeof template === 'string') {
- // id selector
- if (!raw && template.charAt(0) === '#') {
- // id selector can be cached too
- frag = idSelectorCache.get(template);
- if (!frag) {
- node = document.getElementById(template.slice(1));
- if (node) {
- frag = nodeToFragment(node);
- // save selector to cache
- idSelectorCache.put(template, frag);
- }
- }
- } else {
- // normal string template
- frag = stringToFragment(template, raw);
- }
- } else if (template.nodeType) {
- // a direct node
- frag = nodeToFragment(template);
- }
-
- return frag && shouldClone ? cloneNode(frag) : frag;
+/**
+ * Resolve an asset.
+ * This function is used because child instances need access
+ * to assets defined in its ancestor chain.
+ */
+function resolveAsset (
+ options,
+ type,
+ id,
+ warnMissing
+) {
+ /* istanbul ignore if */
+ if (typeof id !== 'string') {
+ return
}
-
-var template = Object.freeze({
- cloneNode: cloneNode,
- parseTemplate: parseTemplate
- });
-
- var html = {
-
- bind: function bind() {
- // a comment node means this is a binding for
- // {{{ inline unescaped html }}}
- if (this.el.nodeType === 8) {
- // hold nodes
- this.nodes = [];
- // replace the placeholder with proper anchor
- this.anchor = createAnchor('v-html');
- replace(this.el, this.anchor);
- }
- },
-
- update: function update(value) {
- value = _toString(value);
- if (this.nodes) {
- this.swap(value);
- } else {
- this.el.innerHTML = value;
- }
- },
-
- swap: function swap(value) {
- // remove old nodes
- var i = this.nodes.length;
- while (i--) {
- remove(this.nodes[i]);
- }
- // convert new value to a fragment
- // do not attempt to retrieve from id selector
- var frag = parseTemplate(value, true, true);
- // save a reference to these nodes so we can remove later
- this.nodes = toArray(frag.childNodes);
- before(frag, this.anchor);
- }
- };
-
- /**
- * Abstraction for a partially-compiled fragment.
- * Can optionally compile content with a child scope.
- *
- * @param {Function} linker
- * @param {Vue} vm
- * @param {DocumentFragment} frag
- * @param {Vue} [host]
- * @param {Object} [scope]
- * @param {Fragment} [parentFrag]
- */
- function Fragment(linker, vm, frag, host, scope, parentFrag) {
- this.children = [];
- this.childFrags = [];
- this.vm = vm;
- this.scope = scope;
- this.inserted = false;
- this.parentFrag = parentFrag;
- if (parentFrag) {
- parentFrag.childFrags.push(this);
- }
- this.unlink = linker(vm, frag, host, scope, this);
- var single = this.single = frag.childNodes.length === 1 &&
- // do not go single mode if the only node is an anchor
- !frag.childNodes[0].__v_anchor;
- if (single) {
- this.node = frag.childNodes[0];
- this.before = singleBefore;
- this.remove = singleRemove;
- } else {
- this.node = createAnchor('fragment-start');
- this.end = createAnchor('fragment-end');
- this.frag = frag;
- prepend(this.node, frag);
- frag.appendChild(this.end);
- this.before = multiBefore;
- this.remove = multiRemove;
- }
- this.node.__v_frag = this;
+ var assets = options[type];
+ var res = assets[id] ||
+ // camelCase ID
+ assets[camelize(id)] ||
+ // Pascal Case ID
+ assets[capitalize(camelize(id))];
+ if ("development" !== 'production' && warnMissing && !res) {
+ warn(
+ 'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
+ options
+ );
+ }
+ return res
+}
+
+/* */
+
+function validateProp (
+ key,
+ propOptions,
+ propsData,
+ vm
+) {
+ var prop = propOptions[key];
+ var absent = !hasOwn(propsData, key);
+ var value = propsData[key];
+ // handle boolean props
+ if (isBooleanType(prop.type)) {
+ if (absent && !hasOwn(prop, 'default')) {
+ value = false;
+ } else if (value === '' || value === hyphenate(key)) {
+ value = true;
+ }
+ }
+ // check default value
+ if (value === undefined) {
+ value = getPropDefaultValue(vm, prop, key);
+ // since the default value is a fresh copy,
+ // make sure to observe it.
+ var prevShouldConvert = observerState.shouldConvert;
+ observerState.shouldConvert = true;
+ observe(value);
+ observerState.shouldConvert = prevShouldConvert;
+ }
+ {
+ assertProp(prop, key, value, vm, absent);
+ }
+ return value
+}
+
+/**
+ * Get the default value of a prop.
+ */
+function getPropDefaultValue (vm, prop, name) {
+ // no default, return undefined
+ if (!hasOwn(prop, 'default')) {
+ return undefined
+ }
+ var def = prop.default;
+ // warn against non-factory defaults for Object & Array
+ if (isObject(def)) {
+ "development" !== 'production' && warn(
+ 'Invalid default value for prop "' + name + '": ' +
+ 'Props with type Object/Array must use a factory function ' +
+ 'to return the default value.',
+ vm
+ );
+ }
+ // call factory function for non-Function types
+ return typeof def === 'function' && prop.type !== Function
+ ? def.call(vm)
+ : def
+}
+
+/**
+ * Assert whether a prop is valid.
+ */
+function assertProp (
+ prop,
+ name,
+ value,
+ vm,
+ absent
+) {
+ if (prop.required && absent) {
+ warn(
+ 'Missing required prop: "' + name + '"',
+ vm
+ );
+ return
+ }
+ if (value == null && !prop.required) {
+ return
+ }
+ var type = prop.type;
+ var valid = !type || type === true;
+ var expectedTypes = [];
+ if (type) {
+ if (!Array.isArray(type)) {
+ type = [type];
+ }
+ for (var i = 0; i < type.length && !valid; i++) {
+ var assertedType = assertType(value, type[i]);
+ expectedTypes.push(assertedType.expectedType);
+ valid = assertedType.valid;
+ }
+ }
+ if (!valid) {
+ warn(
+ 'Invalid prop: type check failed for prop "' + name + '".' +
+ ' Expected ' + expectedTypes.map(capitalize).join(', ') +
+ ', got ' + Object.prototype.toString.call(value).slice(8, -1) + '.',
+ vm
+ );
+ return
+ }
+ var validator = prop.validator;
+ if (validator) {
+ if (!validator(value)) {
+ warn(
+ 'Invalid prop: custom validator check failed for prop "' + name + '".',
+ vm
+ );
+ }
+ }
+}
+
+/**
+ * Assert the type of a value
+ */
+function assertType (value, type) {
+ var valid;
+ var expectedType = getType(type);
+ if (expectedType === 'String') {
+ valid = typeof value === (expectedType = 'string');
+ } else if (expectedType === 'Number') {
+ valid = typeof value === (expectedType = 'number');
+ } else if (expectedType === 'Boolean') {
+ valid = typeof value === (expectedType = 'boolean');
+ } else if (expectedType === 'Function') {
+ valid = typeof value === (expectedType = 'function');
+ } else if (expectedType === 'Object') {
+ valid = isPlainObject(value);
+ } else if (expectedType === 'Array') {
+ valid = Array.isArray(value);
+ } else {
+ valid = value instanceof type;
}
-
- /**
- * Call attach/detach for all components contained within
- * this fragment. Also do so recursively for all child
- * fragments.
- *
- * @param {Function} hook
- */
-
- Fragment.prototype.callHook = function (hook) {
- var i, l;
- for (i = 0, l = this.childFrags.length; i < l; i++) {
- this.childFrags[i].callHook(hook);
- }
- for (i = 0, l = this.children.length; i < l; i++) {
- hook(this.children[i]);
- }
- };
-
- /**
- * Insert fragment before target, single node version
- *
- * @param {Node} target
- * @param {Boolean} withTransition
- */
-
- function singleBefore(target, withTransition) {
- this.inserted = true;
- var method = withTransition !== false ? beforeWithTransition : before;
- method(this.node, target, this.vm);
- if (inDoc(this.node)) {
- this.callHook(attach);
- }
+ return {
+ valid: valid,
+ expectedType: expectedType
}
+}
- /**
- * Remove fragment, single node version
- */
+/**
+ * Use function string name to check built-in types,
+ * because a simple equality check will fail when running
+ * across different vms / iframes.
+ */
+function getType (fn) {
+ var match = fn && fn.toString().match(/^\s*function (\w+)/);
+ return match && match[1]
+}
- function singleRemove() {
- this.inserted = false;
- var shouldCallRemove = inDoc(this.node);
- var self = this;
- this.beforeRemove();
- removeWithTransition(this.node, this.vm, function () {
- if (shouldCallRemove) {
- self.callHook(detach);
- }
- self.destroy();
- });
+function isBooleanType (fn) {
+ if (!Array.isArray(fn)) {
+ return getType(fn) === 'Boolean'
}
-
- /**
- * Insert fragment before target, multi-nodes version
- *
- * @param {Node} target
- * @param {Boolean} withTransition
- */
-
- function multiBefore(target, withTransition) {
- this.inserted = true;
- var vm = this.vm;
- var method = withTransition !== false ? beforeWithTransition : before;
- mapNodeRange(this.node, this.end, function (node) {
- method(node, target, vm);
- });
- if (inDoc(this.node)) {
- this.callHook(attach);
+ for (var i = 0, len = fn.length; i < len; i++) {
+ if (getType(fn[i]) === 'Boolean') {
+ return true
}
}
-
- /**
- * Remove fragment, multi-nodes version
- */
-
- function multiRemove() {
- this.inserted = false;
- var self = this;
- var shouldCallRemove = inDoc(this.node);
- this.beforeRemove();
- removeNodeRange(this.node, this.end, this.vm, this.frag, function () {
- if (shouldCallRemove) {
- self.callHook(detach);
- }
- self.destroy();
- });
- }
-
- /**
- * Prepare the fragment for removal.
- */
-
- Fragment.prototype.beforeRemove = function () {
- var i, l;
- for (i = 0, l = this.childFrags.length; i < l; i++) {
- // call the same method recursively on child
- // fragments, depth-first
- this.childFrags[i].beforeRemove(false);
- }
- for (i = 0, l = this.children.length; i < l; i++) {
- // Call destroy for all contained instances,
- // with remove:false and defer:true.
- // Defer is necessary because we need to
- // keep the children to call detach hooks
- // on them.
- this.children[i].$destroy(false, true);
- }
- var dirs = this.unlink.dirs;
- for (i = 0, l = dirs.length; i < l; i++) {
- // disable the watchers on all the directives
- // so that the rendered content stays the same
- // during removal.
- dirs[i]._watcher && dirs[i]._watcher.teardown();
+ /* istanbul ignore next */
+ return false
+}
+
+
+
+var util = Object.freeze({
+ defineReactive: defineReactive$$1,
+ _toString: _toString,
+ toNumber: toNumber,
+ makeMap: makeMap,
+ isBuiltInTag: isBuiltInTag,
+ remove: remove$1,
+ hasOwn: hasOwn,
+ isPrimitive: isPrimitive,
+ cached: cached,
+ camelize: camelize,
+ capitalize: capitalize,
+ hyphenate: hyphenate,
+ bind: bind$1,
+ toArray: toArray,
+ extend: extend,
+ isObject: isObject,
+ isPlainObject: isPlainObject,
+ toObject: toObject,
+ noop: noop,
+ no: no,
+ genStaticKeys: genStaticKeys,
+ looseEqual: looseEqual,
+ looseIndexOf: looseIndexOf,
+ isReserved: isReserved,
+ def: def,
+ parsePath: parsePath,
+ hasProto: hasProto,
+ inBrowser: inBrowser,
+ UA: UA,
+ isIE: isIE,
+ isIE9: isIE9,
+ isEdge: isEdge,
+ isAndroid: isAndroid,
+ isIOS: isIOS,
+ devtools: devtools,
+ nextTick: nextTick,
+ get _Set () { return _Set; },
+ mergeOptions: mergeOptions,
+ resolveAsset: resolveAsset,
+ get warn () { return warn; },
+ get formatComponentName () { return formatComponentName; },
+ validateProp: validateProp
+});
+
+/* */
+
+function initUse (Vue) {
+ Vue.use = function (plugin) {
+ /* istanbul ignore if */
+ if (plugin.installed) {
+ return
}
+ // additional parameters
+ var args = toArray(arguments, 1);
+ args.unshift(this);
+ if (typeof plugin.install === 'function') {
+ plugin.install.apply(plugin, args);
+ } else {
+ plugin.apply(null, args);
+ }
+ plugin.installed = true;
+ return this
};
+}
- /**
- * Destroy the fragment.
- */
+/* */
- Fragment.prototype.destroy = function () {
- if (this.parentFrag) {
- this.parentFrag.childFrags.$remove(this);
- }
- this.node.__v_frag = null;
- this.unlink();
+function initMixin$1 (Vue) {
+ Vue.mixin = function (mixin) {
+ Vue.options = mergeOptions(Vue.options, mixin);
};
+}
- /**
- * Call attach hook for a Vue instance.
- *
- * @param {Vue} child
- */
-
- function attach(child) {
- if (!child._isAttached && inDoc(child.$el)) {
- child._callHook('attached');
- }
- }
+/* */
+function initExtend (Vue) {
/**
- * Call detach hook for a Vue instance.
- *
- * @param {Vue} child
+ * Each instance constructor, including Vue, has a unique
+ * cid. This enables us to create wrapped "child
+ * constructors" for prototypal inheritance and cache them.
*/
-
- function detach(child) {
- if (child._isAttached && !inDoc(child.$el)) {
- child._callHook('detached');
- }
- }
-
- var linkerCache = new Cache(5000);
+ Vue.cid = 0;
+ var cid = 1;
/**
- * A factory that can be used to create instances of a
- * fragment. Caches the compiled linker if possible.
- *
- * @param {Vue} vm
- * @param {Element|String} el
+ * Class inheritance
*/
- function FragmentFactory(vm, el) {
- this.vm = vm;
- var template;
- var isString = typeof el === 'string';
- if (isString || isTemplate(el) && !el.hasAttribute('v-if')) {
- template = parseTemplate(el, true);
- } else {
- template = document.createDocumentFragment();
- template.appendChild(el);
- }
- this.template = template;
- // linker can be cached, but only for components
- var linker;
- var cid = vm.constructor.cid;
- if (cid > 0) {
- var cacheId = cid + (isString ? el : getOuterHTML(el));
- linker = linkerCache.get(cacheId);
- if (!linker) {
- linker = compile(template, vm.$options, true);
- linkerCache.put(cacheId, linker);
+ Vue.extend = function (extendOptions) {
+ extendOptions = extendOptions || {};
+ var Super = this;
+ var isFirstExtend = Super.cid === 0;
+ if (isFirstExtend && extendOptions._Ctor) {
+ return extendOptions._Ctor
+ }
+ var name = extendOptions.name || Super.options.name;
+ {
+ if (!/^[a-zA-Z][\w-]*$/.test(name)) {
+ warn(
+ 'Invalid component name: "' + name + '". Component names ' +
+ 'can only contain alphanumeric characaters and the hyphen.'
+ );
+ name = null;
}
- } else {
- linker = compile(template, vm.$options, true);
}
- this.linker = linker;
- }
-
- /**
- * Create a fragment instance with given host and scope.
- *
- * @param {Vue} host
- * @param {Object} scope
- * @param {Fragment} parentFrag
- */
-
- FragmentFactory.prototype.create = function (host, scope, parentFrag) {
- var frag = cloneNode(this.template);
- return new Fragment(this.linker, this.vm, frag, host, scope, parentFrag);
+ var Sub = function VueComponent (options) {
+ this._init(options);
+ };
+ Sub.prototype = Object.create(Super.prototype);
+ Sub.prototype.constructor = Sub;
+ Sub.cid = cid++;
+ Sub.options = mergeOptions(
+ Super.options,
+ extendOptions
+ );
+ Sub['super'] = Super;
+ // allow further extension
+ Sub.extend = Super.extend;
+ // create asset registers, so extended classes
+ // can have their private assets too.
+ config._assetTypes.forEach(function (type) {
+ Sub[type] = Super[type];
+ });
+ // enable recursive self-lookup
+ if (name) {
+ Sub.options.components[name] = Sub;
+ }
+ // keep a reference to the super options at extension time.
+ // later at instantiation we can check if Super's options have
+ // been updated.
+ Sub.superOptions = Super.options;
+ Sub.extendOptions = extendOptions;
+ // cache constructor
+ if (isFirstExtend) {
+ extendOptions._Ctor = Sub;
+ }
+ return Sub
};
+}
- var ON = 700;
- var MODEL = 800;
- var BIND = 850;
- var TRANSITION = 1100;
- var EL = 1500;
- var COMPONENT = 1500;
- var PARTIAL = 1750;
- var IF = 2100;
- var FOR = 2200;
- var SLOT = 2300;
-
- var uid$3 = 0;
-
- var vFor = {
-
- priority: FOR,
- terminal: true,
-
- params: ['track-by', 'stagger', 'enter-stagger', 'leave-stagger'],
-
- bind: function bind() {
- // support "item in/of items" syntax
- var inMatch = this.expression.match(/(.*) (?:in|of) (.*)/);
- if (inMatch) {
- var itMatch = inMatch[1].match(/\((.*),(.*)\)/);
- if (itMatch) {
- this.iterator = itMatch[1].trim();
- this.alias = itMatch[2].trim();
- } else {
- this.alias = inMatch[1].trim();
- }
- this.expression = inMatch[2];
- }
-
- if (!this.alias) {
- 'development' !== 'production' && warn('Invalid v-for expression "' + this.descriptor.raw + '": ' + 'alias is required.', this.vm);
- return;
- }
-
- // uid as a cache identifier
- this.id = '__v-for__' + ++uid$3;
-
- // check if this is an option list,
- // so that we know if we need to update the <select>'s
- // v-model when the option list has changed.
- // because v-model has a lower priority than v-for,
- // the v-model is not bound here yet, so we have to
- // retrive it in the actual updateModel() function.
- var tag = this.el.tagName;
- this.isOption = (tag === 'OPTION' || tag === 'OPTGROUP') && this.el.parentNode.tagName === 'SELECT';
-
- // setup anchor nodes
- this.start = createAnchor('v-for-start');
- this.end = createAnchor('v-for-end');
- replace(this.el, this.end);
- before(this.start, this.end);
-
- // cache
- this.cache = Object.create(null);
-
- // fragment factory
- this.factory = new FragmentFactory(this.vm, this.el);
- },
-
- update: function update(data) {
- this.diff(data);
- this.updateRef();
- this.updateModel();
- },
-
- /**
- * Diff, based on new data and old data, determine the
- * minimum amount of DOM manipulations needed to make the
- * DOM reflect the new data Array.
- *
- * The algorithm diffs the new data Array by storing a
- * hidden reference to an owner vm instance on previously
- * seen data. This allows us to achieve O(n) which is
- * better than a levenshtein distance based algorithm,
- * which is O(m * n).
- *
- * @param {Array} data
- */
-
- diff: function diff(data) {
- // check if the Array was converted from an Object
- var item = data[0];
- var convertedFromObject = this.fromObject = isObject(item) && hasOwn(item, '$key') && hasOwn(item, '$value');
-
- var trackByKey = this.params.trackBy;
- var oldFrags = this.frags;
- var frags = this.frags = new Array(data.length);
- var alias = this.alias;
- var iterator = this.iterator;
- var start = this.start;
- var end = this.end;
- var inDocument = inDoc(start);
- var init = !oldFrags;
- var i, l, frag, key, value, primitive;
-
- // First pass, go through the new Array and fill up
- // the new frags array. If a piece of data has a cached
- // instance for it, we reuse it. Otherwise build a new
- // instance.
- for (i = 0, l = data.length; i < l; i++) {
- item = data[i];
- key = convertedFromObject ? item.$key : null;
- value = convertedFromObject ? item.$value : item;
- primitive = !isObject(value);
- frag = !init && this.getCachedFrag(value, i, key);
- if (frag) {
- // reusable fragment
- frag.reused = true;
- // update $index
- frag.scope.$index = i;
- // update $key
- if (key) {
- frag.scope.$key = key;
- }
- // update iterator
- if (iterator) {
- frag.scope[iterator] = key !== null ? key : i;
- }
- // update data for track-by, object repeat &
- // primitive values.
- if (trackByKey || convertedFromObject || primitive) {
- withoutConversion(function () {
- frag.scope[alias] = value;
- });
- }
- } else {
- // new isntance
- frag = this.create(value, alias, i, key);
- frag.fresh = !init;
- }
- frags[i] = frag;
- if (init) {
- frag.before(end);
- }
- }
-
- // we're done for the initial render.
- if (init) {
- return;
- }
-
- // Second pass, go through the old fragments and
- // destroy those who are not reused (and remove them
- // from cache)
- var removalIndex = 0;
- var totalRemoved = oldFrags.length - frags.length;
- // when removing a large number of fragments, watcher removal
- // turns out to be a perf bottleneck, so we batch the watcher
- // removals into a single filter call!
- this.vm._vForRemoving = true;
- for (i = 0, l = oldFrags.length; i < l; i++) {
- frag = oldFrags[i];
- if (!frag.reused) {
- this.deleteCachedFrag(frag);
- this.remove(frag, removalIndex++, totalRemoved, inDocument);
- }
- }
- this.vm._vForRemoving = false;
- if (removalIndex) {
- this.vm._watchers = this.vm._watchers.filter(function (w) {
- return w.active;
- });
- }
-
- // Final pass, move/insert new fragments into the
- // right place.
- var targetPrev, prevEl, currentPrev;
- var insertionIndex = 0;
- for (i = 0, l = frags.length; i < l; i++) {
- frag = frags[i];
- // this is the frag that we should be after
- targetPrev = frags[i - 1];
- prevEl = targetPrev ? targetPrev.staggerCb ? targetPrev.staggerAnchor : targetPrev.end || targetPrev.node : start;
- if (frag.reused && !frag.staggerCb) {
- currentPrev = findPrevFrag(frag, start, this.id);
- if (currentPrev !== targetPrev && (!currentPrev ||
- // optimization for moving a single item.
- // thanks to suggestions by @livoras in #1807
- findPrevFrag(currentPrev, start, this.id) !== targetPrev)) {
- this.move(frag, prevEl);
- }
- } else {
- // new instance, or still in stagger.
- // insert with updated stagger index.
- this.insert(frag, insertionIndex++, prevEl, inDocument);
- }
- frag.reused = frag.fresh = false;
- }
- },
-
- /**
- * Create a new fragment instance.
- *
- * @param {*} value
- * @param {String} alias
- * @param {Number} index
- * @param {String} [key]
- * @return {Fragment}
- */
-
- create: function create(value, alias, index, key) {
- var host = this._host;
- // create iteration scope
- var parentScope = this._scope || this.vm;
- var scope = Object.create(parentScope);
- // ref holder for the scope
- scope.$refs = Object.create(parentScope.$refs);
- scope.$els = Object.create(parentScope.$els);
- // make sure point $parent to parent scope
- scope.$parent = parentScope;
- // for two-way binding on alias
- scope.$forContext = this;
- // define scope properties
- // important: define the scope alias without forced conversion
- // so that frozen data structures remain non-reactive.
- withoutConversion(function () {
- defineReactive(scope, alias, value);
- });
- defineReactive(scope, '$index', index);
- if (key) {
- defineReactive(scope, '$key', key);
- } else if (scope.$key) {
- // avoid accidental fallback
- def(scope, '$key', null);
- }
- if (this.iterator) {
- defineReactive(scope, this.iterator, key !== null ? key : index);
- }
- var frag = this.factory.create(host, scope, this._frag);
- frag.forId = this.id;
- this.cacheFrag(value, frag, index, key);
- return frag;
- },
-
- /**
- * Update the v-ref on owner vm.
- */
-
- updateRef: function updateRef() {
- var ref = this.descriptor.ref;
- if (!ref) return;
- var hash = (this._scope || this.vm).$refs;
- var refs;
- if (!this.fromObject) {
- refs = this.frags.map(findVmFromFrag);
- } else {
- refs = {};
- this.frags.forEach(function (frag) {
- refs[frag.scope.$key] = findVmFromFrag(frag);
- });
- }
- hash[ref] = refs;
- },
-
- /**
- * For option lists, update the containing v-model on
- * parent <select>.
- */
-
- updateModel: function updateModel() {
- if (this.isOption) {
- var parent = this.start.parentNode;
- var model = parent && parent.__v_model;
- if (model) {
- model.forceUpdate();
- }
- }
- },
+/* */
- /**
- * Insert a fragment. Handles staggering.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Node} prevEl
- * @param {Boolean} inDocument
- */
-
- insert: function insert(frag, index, prevEl, inDocument) {
- if (frag.staggerCb) {
- frag.staggerCb.cancel();
- frag.staggerCb = null;
- }
- var staggerAmount = this.getStagger(frag, index, null, 'enter');
- if (inDocument && staggerAmount) {
- // create an anchor and insert it synchronously,
- // so that we can resolve the correct order without
- // worrying about some elements not inserted yet
- var anchor = frag.staggerAnchor;
- if (!anchor) {
- anchor = frag.staggerAnchor = createAnchor('stagger-anchor');
- anchor.__v_frag = frag;
- }
- after(anchor, prevEl);
- var op = frag.staggerCb = cancellable(function () {
- frag.staggerCb = null;
- frag.before(anchor);
- remove(anchor);
- });
- setTimeout(op, staggerAmount);
+function initAssetRegisters (Vue) {
+ /**
+ * Create asset registration methods.
+ */
+ config._assetTypes.forEach(function (type) {
+ Vue[type] = function (
+ id,
+ definition
+ ) {
+ if (!definition) {
+ return this.options[type + 's'][id]
} else {
- var target = prevEl.nextSibling;
/* istanbul ignore if */
- if (!target) {
- // reset end anchor position in case the position was messed up
- // by an external drag-n-drop library.
- after(this.end, prevEl);
- target = this.end;
- }
- frag.before(target);
- }
- },
-
- /**
- * Remove a fragment. Handles staggering.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Number} total
- * @param {Boolean} inDocument
- */
-
- remove: function remove(frag, index, total, inDocument) {
- if (frag.staggerCb) {
- frag.staggerCb.cancel();
- frag.staggerCb = null;
- // it's not possible for the same frag to be removed
- // twice, so if we have a pending stagger callback,
- // it means this frag is queued for enter but removed
- // before its transition started. Since it is already
- // destroyed, we can just leave it in detached state.
- return;
- }
- var staggerAmount = this.getStagger(frag, index, total, 'leave');
- if (inDocument && staggerAmount) {
- var op = frag.staggerCb = cancellable(function () {
- frag.staggerCb = null;
- frag.remove();
- });
- setTimeout(op, staggerAmount);
- } else {
- frag.remove();
- }
- },
-
- /**
- * Move a fragment to a new position.
- * Force no transition.
- *
- * @param {Fragment} frag
- * @param {Node} prevEl
- */
-
- move: function move(frag, prevEl) {
- // fix a common issue with Sortable:
- // if prevEl doesn't have nextSibling, this means it's
- // been dragged after the end anchor. Just re-position
- // the end anchor to the end of the container.
- /* istanbul ignore if */
- if (!prevEl.nextSibling) {
- this.end.parentNode.appendChild(this.end);
- }
- frag.before(prevEl.nextSibling, false);
- },
-
- /**
- * Cache a fragment using track-by or the object key.
- *
- * @param {*} value
- * @param {Fragment} frag
- * @param {Number} index
- * @param {String} [key]
- */
-
- cacheFrag: function cacheFrag(value, frag, index, key) {
- var trackByKey = this.params.trackBy;
- var cache = this.cache;
- var primitive = !isObject(value);
- var id;
- if (key || trackByKey || primitive) {
- id = getTrackByKey(index, key, value, trackByKey);
- if (!cache[id]) {
- cache[id] = frag;
- } else if (trackByKey !== '$index') {
- 'development' !== 'production' && this.warnDuplicate(value);
- }
- } else {
- id = this.id;
- if (hasOwn(value, id)) {
- if (value[id] === null) {
- value[id] = frag;
- } else {
- 'development' !== 'production' && this.warnDuplicate(value);
+ {
+ if (type === 'component' && config.isReservedTag(id)) {
+ warn(
+ 'Do not use built-in or reserved HTML elements as component ' +
+ 'id: ' + id
+ );
}
- } else if (Object.isExtensible(value)) {
- def(value, id, frag);
- } else if ('development' !== 'production') {
- warn('Frozen v-for objects cannot be automatically tracked, make sure to ' + 'provide a track-by key.');
- }
- }
- frag.raw = value;
- },
-
- /**
- * Get a cached fragment from the value/index/key
- *
- * @param {*} value
- * @param {Number} index
- * @param {String} key
- * @return {Fragment}
- */
-
- getCachedFrag: function getCachedFrag(value, index, key) {
- var trackByKey = this.params.trackBy;
- var primitive = !isObject(value);
- var frag;
- if (key || trackByKey || primitive) {
- var id = getTrackByKey(index, key, value, trackByKey);
- frag = this.cache[id];
- } else {
- frag = value[this.id];
- }
- if (frag && (frag.reused || frag.fresh)) {
- 'development' !== 'production' && this.warnDuplicate(value);
- }
- return frag;
- },
-
- /**
- * Delete a fragment from cache.
- *
- * @param {Fragment} frag
- */
-
- deleteCachedFrag: function deleteCachedFrag(frag) {
- var value = frag.raw;
- var trackByKey = this.params.trackBy;
- var scope = frag.scope;
- var index = scope.$index;
- // fix #948: avoid accidentally fall through to
- // a parent repeater which happens to have $key.
- var key = hasOwn(scope, '$key') && scope.$key;
- var primitive = !isObject(value);
- if (trackByKey || key || primitive) {
- var id = getTrackByKey(index, key, value, trackByKey);
- this.cache[id] = null;
- } else {
- value[this.id] = null;
- frag.raw = null;
- }
- },
-
- /**
- * Get the stagger amount for an insertion/removal.
- *
- * @param {Fragment} frag
- * @param {Number} index
- * @param {Number} total
- * @param {String} type
- */
-
- getStagger: function getStagger(frag, index, total, type) {
- type = type + 'Stagger';
- var trans = frag.node.__v_trans;
- var hooks = trans && trans.hooks;
- var hook = hooks && (hooks[type] || hooks.stagger);
- return hook ? hook.call(frag, index, total) : index * parseInt(this.params[type] || this.params.stagger, 10);
- },
-
- /**
- * Pre-process the value before piping it through the
- * filters. This is passed to and called by the watcher.
- */
-
- _preProcess: function _preProcess(value) {
- // regardless of type, store the un-filtered raw value.
- this.rawValue = value;
- return value;
- },
-
- /**
- * Post-process the value after it has been piped through
- * the filters. This is passed to and called by the watcher.
- *
- * It is necessary for this to be called during the
- * watcher's dependency collection phase because we want
- * the v-for to update when the source Object is mutated.
- */
-
- _postProcess: function _postProcess(value) {
- if (isArray(value)) {
- return value;
- } else if (isPlainObject(value)) {
- // convert plain object to array.
- var keys = Object.keys(value);
- var i = keys.length;
- var res = new Array(i);
- var key;
- while (i--) {
- key = keys[i];
- res[i] = {
- $key: key,
- $value: value[key]
- };
}
- return res;
- } else {
- if (typeof value === 'number' && !isNaN(value)) {
- value = range(value);
+ if (type === 'component' && isPlainObject(definition)) {
+ definition.name = definition.name || id;
+ definition = Vue.extend(definition);
}
- return value || [];
- }
- },
-
- unbind: function unbind() {
- if (this.descriptor.ref) {
- (this._scope || this.vm).$refs[this.descriptor.ref] = null;
- }
- if (this.frags) {
- var i = this.frags.length;
- var frag;
- while (i--) {
- frag = this.frags[i];
- this.deleteCachedFrag(frag);
- frag.destroy();
+ if (type === 'directive' && typeof definition === 'function') {
+ definition = { bind: definition, update: definition };
}
+ this.options[type + 's'][id] = definition;
+ return definition
}
- }
- };
-
- /**
- * Helper to find the previous element that is a fragment
- * anchor. This is necessary because a destroyed frag's
- * element could still be lingering in the DOM before its
- * leaving transition finishes, but its inserted flag
- * should have been set to false so we can skip them.
- *
- * If this is a block repeat, we want to make sure we only
- * return frag that is bound to this v-for. (see #929)
- *
- * @param {Fragment} frag
- * @param {Comment|Text} anchor
- * @param {String} id
- * @return {Fragment}
- */
-
- function findPrevFrag(frag, anchor, id) {
- var el = frag.node.previousSibling;
- /* istanbul ignore if */
- if (!el) return;
- frag = el.__v_frag;
- while ((!frag || frag.forId !== id || !frag.inserted) && el !== anchor) {
- el = el.previousSibling;
- /* istanbul ignore if */
- if (!el) return;
- frag = el.__v_frag;
- }
- return frag;
- }
-
- /**
- * Find a vm from a fragment.
- *
- * @param {Fragment} frag
- * @return {Vue|undefined}
- */
-
- function findVmFromFrag(frag) {
- var node = frag.node;
- // handle multi-node frag
- if (frag.end) {
- while (!node.__vue__ && node !== frag.end && node.nextSibling) {
- node = node.nextSibling;
+ };
+ });
+}
+
+var KeepAlive = {
+ name: 'keep-alive',
+ abstract: true,
+ created: function created () {
+ this.cache = Object.create(null);
+ },
+ render: function render () {
+ var vnode = getFirstComponentChild(this.$slots.default);
+ if (vnode && vnode.componentOptions) {
+ var opts = vnode.componentOptions;
+ var key = vnode.key == null
+ // same constructor may get registered as different local components
+ // so cid alone is not enough (#3269)
+ ? opts.Ctor.cid + '::' + opts.tag
+ : vnode.key;
+ if (this.cache[key]) {
+ vnode.child = this.cache[key].child;
+ } else {
+ this.cache[key] = vnode;
}
+ vnode.data.keepAlive = true;
}
- return node.__vue__;
- }
-
- /**
- * Create a range array from given number.
- *
- * @param {Number} n
- * @return {Array}
- */
+ return vnode
+ },
+ destroyed: function destroyed () {
+ var this$1 = this;
- function range(n) {
- var i = -1;
- var ret = new Array(Math.floor(n));
- while (++i < n) {
- ret[i] = i;
+ for (var key in this.cache) {
+ var vnode = this$1.cache[key];
+ callHook(vnode.child, 'deactivated');
+ vnode.child.$destroy();
}
- return ret;
}
+};
- /**
- * Get the track by key for an item.
- *
- * @param {Number} index
- * @param {String} key
- * @param {*} value
- * @param {String} [trackByKey]
- */
+var builtInComponents = {
+ KeepAlive: KeepAlive
+};
- function getTrackByKey(index, key, value, trackByKey) {
- return trackByKey ? trackByKey === '$index' ? index : trackByKey.charAt(0).match(/\w/) ? getPath(value, trackByKey) : value[trackByKey] : key || value;
- }
+/* */
- if ('development' !== 'production') {
- vFor.warnDuplicate = function (value) {
- warn('Duplicate value found in v-for="' + this.descriptor.raw + '": ' + JSON.stringify(value) + '. Use track-by="$index" if ' + 'you are expecting duplicate values.', this.vm);
+function initGlobalAPI (Vue) {
+ // config
+ var configDef = {};
+ configDef.get = function () { return config; };
+ {
+ configDef.set = function () {
+ warn(
+ 'Do not replace the Vue.config object, set individual fields instead.'
+ );
};
}
+ Object.defineProperty(Vue, 'config', configDef);
+ Vue.util = util;
+ Vue.set = set;
+ Vue.delete = del;
+ Vue.nextTick = nextTick;
- var vIf = {
+ Vue.options = Object.create(null);
+ config._assetTypes.forEach(function (type) {
+ Vue.options[type + 's'] = Object.create(null);
+ });
- priority: IF,
- terminal: true,
+ extend(Vue.options.components, builtInComponents);
- bind: function bind() {
- var el = this.el;
- if (!el.__vue__) {
- // check else block
- var next = el.nextElementSibling;
- if (next && getAttr(next, 'v-else') !== null) {
- remove(next);
- this.elseEl = next;
- }
- // check main block
- this.anchor = createAnchor('v-if');
- replace(el, this.anchor);
- } else {
- 'development' !== 'production' && warn('v-if="' + this.expression + '" cannot be ' + 'used on an instance root element.', this.vm);
- this.invalid = true;
- }
- },
+ initUse(Vue);
+ initMixin$1(Vue);
+ initExtend(Vue);
+ initAssetRegisters(Vue);
+}
- update: function update(value) {
- if (this.invalid) return;
- if (value) {
- if (!this.frag) {
- this.insert();
- }
- } else {
- this.remove();
- }
- },
+initGlobalAPI(Vue$3);
- insert: function insert() {
- if (this.elseFrag) {
- this.elseFrag.remove();
- this.elseFrag = null;
- }
- // lazy init factory
- if (!this.factory) {
- this.factory = new FragmentFactory(this.vm, this.el);
- }
- this.frag = this.factory.create(this._host, this._scope, this._frag);
- this.frag.before(this.anchor);
- },
+Object.defineProperty(Vue$3.prototype, '$isServer', {
+ get: function () { return config._isServer; }
+});
- remove: function remove() {
- if (this.frag) {
- this.frag.remove();
- this.frag = null;
- }
- if (this.elseEl && !this.elseFrag) {
- if (!this.elseFactory) {
- this.elseFactory = new FragmentFactory(this.elseEl._context || this.vm, this.elseEl);
- }
- this.elseFrag = this.elseFactory.create(this._host, this._scope, this._frag);
- this.elseFrag.before(this.anchor);
- }
- },
+Vue$3.version = '2.0.3';
- unbind: function unbind() {
- if (this.frag) {
- this.frag.destroy();
- }
- if (this.elseFrag) {
- this.elseFrag.destroy();
- }
- }
- };
+/* */
- var show = {
+// attributes that should be using props for binding
+var mustUseProp = makeMap('value,selected,checked,muted');
- bind: function bind() {
- // check else block
- var next = this.el.nextElementSibling;
- if (next && getAttr(next, 'v-else') !== null) {
- this.elseEl = next;
- }
- },
+var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');
- update: function update(value) {
- this.apply(this.el, value);
- if (this.elseEl) {
- this.apply(this.elseEl, !value);
- }
- },
+var isBooleanAttr = makeMap(
+ 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +
+ 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +
+ 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +
+ 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +
+ 'required,reversed,scoped,seamless,selected,sortable,translate,' +
+ 'truespeed,typemustmatch,visible'
+);
- apply: function apply(el, value) {
- if (inDoc(el)) {
- applyTransition(el, value ? 1 : -1, toggle, this.vm);
- } else {
- toggle();
- }
- function toggle() {
- el.style.display = value ? '' : 'none';
- }
- }
- };
+var isAttr = makeMap(
+ 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
+ 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
+ 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
+ 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
+ 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
+ 'form,formaction,headers,<th>,height,hidden,high,href,hreflang,http-equiv,' +
+ 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
+ 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
+ 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
+ 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
+ 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
+ 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
+ 'target,title,type,usemap,value,width,wrap'
+);
- var text$2 = {
-
- bind: function bind() {
- var self = this;
- var el = this.el;
- var isRange = el.type === 'range';
- var lazy = this.params.lazy;
- var number = this.params.number;
- var debounce = this.params.debounce;
-
- // handle composition events.
- // http://blog.evanyou.me/2014/01/03/composition-event/
- // skip this for Android because it handles composition
- // events quite differently. Android doesn't trigger
- // composition events for language input methods e.g.
- // Chinese, but instead triggers them for spelling
- // suggestions... (see Discussion/#162)
- var composing = false;
- if (!isAndroid && !isRange) {
- this.on('compositionstart', function () {
- composing = true;
- });
- this.on('compositionend', function () {
- composing = false;
- // in IE11 the "compositionend" event fires AFTER
- // the "input" event, so the input handler is blocked
- // at the end... have to call it here.
- //
- // #1327: in lazy mode this is unecessary.
- if (!lazy) {
- self.listener();
- }
- });
- }
- // prevent messing with the input when user is typing,
- // and force update on blur.
- this.focused = false;
- if (!isRange && !lazy) {
- this.on('focus', function () {
- self.focused = true;
- });
- this.on('blur', function () {
- self.focused = false;
- // do not sync value after fragment removal (#2017)
- if (!self._frag || self._frag.inserted) {
- self.rawListener();
- }
- });
- }
- // Now attach the main listener
- this.listener = this.rawListener = function () {
- if (composing || !self._bound) {
- return;
- }
- var val = number || isRange ? toNumber(el.value) : el.value;
- self.set(val);
- // force update on next tick to avoid lock & same value
- // also only update when user is not typing
- nextTick(function () {
- if (self._bound && !self.focused) {
- self.update(self._watcher.value);
- }
- });
- };
+var xlinkNS = 'http://www.w3.org/1999/xlink';
- // apply debounce
- if (debounce) {
- this.listener = _debounce(this.listener, debounce);
- }
-
- // Support jQuery events, since jQuery.trigger() doesn't
- // trigger native events in some cases and some plugins
- // rely on $.trigger()
- //
- // We want to make sure if a listener is attached using
- // jQuery, it is also removed with jQuery, that's why
- // we do the check for each directive instance and
- // store that check result on itself. This also allows
- // easier test coverage control by unsetting the global
- // jQuery variable in tests.
- this.hasjQuery = typeof jQuery === 'function';
- if (this.hasjQuery) {
- var method = jQuery.fn.on ? 'on' : 'bind';
- jQuery(el)[method]('change', this.rawListener);
- if (!lazy) {
- jQuery(el)[method]('input', this.listener);
- }
- } else {
- this.on('change', this.rawListener);
- if (!lazy) {
- this.on('input', this.listener);
- }
- }
+var isXlink = function (name) {
+ return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'
+};
- // IE9 doesn't fire input event on backspace/del/cut
- if (!lazy && isIE9) {
- this.on('cut', function () {
- nextTick(self.listener);
- });
- this.on('keyup', function (e) {
- if (e.keyCode === 46 || e.keyCode === 8) {
- self.listener();
- }
- });
- }
+var getXlinkProp = function (name) {
+ return isXlink(name) ? name.slice(6, name.length) : ''
+};
- // set initial value if present
- if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) {
- this.afterBind = this.listener;
- }
- },
+var isFalsyAttrValue = function (val) {
+ return val == null || val === false
+};
- update: function update(value) {
- // #3029 only update when the value changes. This prevent
- // browsers from overwriting values like selectionStart
- value = _toString(value);
- if (value !== this.el.value) this.el.value = value;
- },
+/* */
- unbind: function unbind() {
- var el = this.el;
- if (this.hasjQuery) {
- var method = jQuery.fn.off ? 'off' : 'unbind';
- jQuery(el)[method]('change', this.listener);
- jQuery(el)[method]('input', this.listener);
- }
+function genClassForVnode (vnode) {
+ var data = vnode.data;
+ var parentNode = vnode;
+ var childNode = vnode;
+ while (childNode.child) {
+ childNode = childNode.child._vnode;
+ if (childNode.data) {
+ data = mergeClassData(childNode.data, data);
}
- };
-
- var radio = {
-
- bind: function bind() {
- var self = this;
- var el = this.el;
-
- this.getValue = function () {
- // value overwrite via v-bind:value
- if (el.hasOwnProperty('_value')) {
- return el._value;
- }
- var val = el.value;
- if (self.params.number) {
- val = toNumber(val);
- }
- return val;
- };
-
- this.listener = function () {
- self.set(self.getValue());
- };
- this.on('change', this.listener);
-
- if (el.hasAttribute('checked')) {
- this.afterBind = this.listener;
- }
- },
-
- update: function update(value) {
- this.el.checked = looseEqual(value, this.getValue());
+ }
+ while ((parentNode = parentNode.parent)) {
+ if (parentNode.data) {
+ data = mergeClassData(data, parentNode.data);
}
- };
+ }
+ return genClassFromData(data)
+}
- var select = {
+function mergeClassData (child, parent) {
+ return {
+ staticClass: concat(child.staticClass, parent.staticClass),
+ class: child.class
+ ? [child.class, parent.class]
+ : parent.class
+ }
+}
- bind: function bind() {
- var _this = this;
+function genClassFromData (data) {
+ var dynamicClass = data.class;
+ var staticClass = data.staticClass;
+ if (staticClass || dynamicClass) {
+ return concat(staticClass, stringifyClass(dynamicClass))
+ }
+ /* istanbul ignore next */
+ return ''
+}
- var self = this;
- var el = this.el;
+function concat (a, b) {
+ return a ? b ? (a + ' ' + b) : a : (b || '')
+}
- // method to force update DOM using latest value.
- this.forceUpdate = function () {
- if (self._watcher) {
- self.update(self._watcher.get());
+function stringifyClass (value) {
+ var res = '';
+ if (!value) {
+ return res
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (Array.isArray(value)) {
+ var stringified;
+ for (var i = 0, l = value.length; i < l; i++) {
+ if (value[i]) {
+ if ((stringified = stringifyClass(value[i]))) {
+ res += stringified + ' ';
}
- };
-
- // check if this is a multiple select
- var multiple = this.multiple = el.hasAttribute('multiple');
-
- // attach listener
- this.listener = function () {
- var value = getValue(el, multiple);
- value = self.params.number ? isArray(value) ? value.map(toNumber) : toNumber(value) : value;
- self.set(value);
- };
- this.on('change', this.listener);
-
- // if has initial value, set afterBind
- var initValue = getValue(el, multiple, true);
- if (multiple && initValue.length || !multiple && initValue !== null) {
- this.afterBind = this.listener;
- }
-
- // All major browsers except Firefox resets
- // selectedIndex with value -1 to 0 when the element
- // is appended to a new parent, therefore we have to
- // force a DOM update whenever that happens...
- this.vm.$on('hook:attached', function () {
- nextTick(_this.forceUpdate);
- });
- if (!inDoc(el)) {
- nextTick(this.forceUpdate);
- }
- },
-
- update: function update(value) {
- var el = this.el;
- el.selectedIndex = -1;
- var multi = this.multiple && isArray(value);
- var options = el.options;
- var i = options.length;
- var op, val;
- while (i--) {
- op = options[i];
- val = op.hasOwnProperty('_value') ? op._value : op.value;
- /* eslint-disable eqeqeq */
- op.selected = multi ? indexOf$1(value, val) > -1 : looseEqual(value, val);
- /* eslint-enable eqeqeq */
}
- },
-
- unbind: function unbind() {
- /* istanbul ignore next */
- this.vm.$off('hook:attached', this.forceUpdate);
}
- };
-
- /**
- * Get select value
- *
- * @param {SelectElement} el
- * @param {Boolean} multi
- * @param {Boolean} init
- * @return {Array|*}
- */
-
- function getValue(el, multi, init) {
- var res = multi ? [] : null;
- var op, val, selected;
- for (var i = 0, l = el.options.length; i < l; i++) {
- op = el.options[i];
- selected = init ? op.hasAttribute('selected') : op.selected;
- if (selected) {
- val = op.hasOwnProperty('_value') ? op._value : op.value;
- if (multi) {
- res.push(val);
- } else {
- return val;
- }
- }
+ return res.slice(0, -1)
+ }
+ if (isObject(value)) {
+ for (var key in value) {
+ if (value[key]) { res += key + ' '; }
}
- return res;
+ return res.slice(0, -1)
+ }
+ /* istanbul ignore next */
+ return res
+}
+
+/* */
+
+var namespaceMap = {
+ svg: 'http://www.w3.org/2000/svg',
+ math: 'http://www.w3.org/1998/Math/MathML'
+};
+
+var isHTMLTag = makeMap(
+ 'html,body,base,head,link,meta,style,title,' +
+ 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
+ 'div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,' +
+ 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
+ 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
+ 'embed,object,param,source,canvas,script,noscript,del,ins,' +
+ 'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
+ 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
+ 'output,progress,select,textarea,' +
+ 'details,dialog,menu,menuitem,summary,' +
+ 'content,element,shadow,template'
+);
+
+var isUnaryTag = makeMap(
+ 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
+ 'link,meta,param,source,track,wbr',
+ true
+);
+
+// Elements that you can, intentionally, leave open
+// (and which close themselves)
+var canBeLeftOpenTag = makeMap(
+ 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source',
+ true
+);
+
+// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
+// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
+var isNonPhrasingTag = makeMap(
+ 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
+ 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
+ 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
+ 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
+ 'title,tr,track',
+ true
+);
+
+// this map is intentionally selective, only covering SVG elements that may
+// contain child elements.
+var isSVG = makeMap(
+ 'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,' +
+ 'font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +
+ 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',
+ true
+);
+
+var isPreTag = function (tag) { return tag === 'pre'; };
+
+var isReservedTag = function (tag) {
+ return isHTMLTag(tag) || isSVG(tag)
+};
+
+function getTagNamespace (tag) {
+ if (isSVG(tag)) {
+ return 'svg'
+ }
+ // basic support for MathML
+ // note it doesn't support other MathML elements being component roots
+ if (tag === 'math') {
+ return 'math'
+ }
+}
+
+var unknownElementCache = Object.create(null);
+function isUnknownElement (tag) {
+ /* istanbul ignore if */
+ if (!inBrowser) {
+ return true
+ }
+ if (isReservedTag(tag)) {
+ return false
+ }
+ tag = tag.toLowerCase();
+ /* istanbul ignore if */
+ if (unknownElementCache[tag] != null) {
+ return unknownElementCache[tag]
+ }
+ var el = document.createElement(tag);
+ if (tag.indexOf('-') > -1) {
+ // http://stackoverflow.com/a/28210364/1070244
+ return (unknownElementCache[tag] = (
+ el.constructor === window.HTMLUnknownElement ||
+ el.constructor === window.HTMLElement
+ ))
+ } else {
+ return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))
}
+}
- /**
- * Native Array.indexOf uses strict equal, but in this
- * case we need to match string/numbers with custom equal.
- *
- * @param {Array} arr
- * @param {*} val
- */
+/* */
- function indexOf$1(arr, val) {
- var i = arr.length;
- while (i--) {
- if (looseEqual(arr[i], val)) {
- return i;
+/**
+ * Query an element selector if it's not an element already.
+ */
+function query (el) {
+ if (typeof el === 'string') {
+ var selector = el;
+ el = document.querySelector(el);
+ if (!el) {
+ "development" !== 'production' && warn(
+ 'Cannot find element: ' + selector
+ );
+ return document.createElement('div')
+ }
+ }
+ return el
+}
+
+/* */
+
+function createElement$1 (tagName, vnode) {
+ var elm = document.createElement(tagName);
+ if (tagName !== 'select') {
+ return elm
+ }
+ if (vnode.data && vnode.data.attrs && 'multiple' in vnode.data.attrs) {
+ elm.setAttribute('multiple', 'multiple');
+ }
+ return elm
+}
+
+function createElementNS (namespace, tagName) {
+ return document.createElementNS(namespaceMap[namespace], tagName)
+}
+
+function createTextNode (text) {
+ return document.createTextNode(text)
+}
+
+function createComment (text) {
+ return document.createComment(text)
+}
+
+function insertBefore (parentNode, newNode, referenceNode) {
+ parentNode.insertBefore(newNode, referenceNode);
+}
+
+function removeChild (node, child) {
+ node.removeChild(child);
+}
+
+function appendChild (node, child) {
+ node.appendChild(child);
+}
+
+function parentNode (node) {
+ return node.parentNode
+}
+
+function nextSibling (node) {
+ return node.nextSibling
+}
+
+function tagName (node) {
+ return node.tagName
+}
+
+function setTextContent (node, text) {
+ node.textContent = text;
+}
+
+function childNodes (node) {
+ return node.childNodes
+}
+
+function setAttribute (node, key, val) {
+ node.setAttribute(key, val);
+}
+
+
+var nodeOps = Object.freeze({
+ createElement: createElement$1,
+ createElementNS: createElementNS,
+ createTextNode: createTextNode,
+ createComment: createComment,
+ insertBefore: insertBefore,
+ removeChild: removeChild,
+ appendChild: appendChild,
+ parentNode: parentNode,
+ nextSibling: nextSibling,
+ tagName: tagName,
+ setTextContent: setTextContent,
+ childNodes: childNodes,
+ setAttribute: setAttribute
+});
+
+/* */
+
+var ref = {
+ create: function create (_, vnode) {
+ registerRef(vnode);
+ },
+ update: function update (oldVnode, vnode) {
+ if (oldVnode.data.ref !== vnode.data.ref) {
+ registerRef(oldVnode, true);
+ registerRef(vnode);
+ }
+ },
+ destroy: function destroy (vnode) {
+ registerRef(vnode, true);
+ }
+};
+
+function registerRef (vnode, isRemoval) {
+ var key = vnode.data.ref;
+ if (!key) { return }
+
+ var vm = vnode.context;
+ var ref = vnode.child || vnode.elm;
+ var refs = vm.$refs;
+ if (isRemoval) {
+ if (Array.isArray(refs[key])) {
+ remove$1(refs[key], ref);
+ } else if (refs[key] === ref) {
+ refs[key] = undefined;
+ }
+ } else {
+ if (vnode.data.refInFor) {
+ if (Array.isArray(refs[key])) {
+ refs[key].push(ref);
+ } else {
+ refs[key] = [ref];
}
+ } else {
+ refs[key] = ref;
}
- return -1;
}
+}
- var checkbox = {
+/**
+ * Virtual DOM patching algorithm based on Snabbdom by
+ * Simon Friis Vindum (@paldepind)
+ * Licensed under the MIT License
+ * https://github.com/paldepind/snabbdom/blob/master/LICENSE
+ *
+ * modified by Evan You (@yyx990803)
+ *
- bind: function bind() {
- var self = this;
- var el = this.el;
+/*
+ * Not type-checking this because this file is perf-critical and the cost
+ * of making flow understand it is not worth it.
+ */
- this.getValue = function () {
- return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
- };
+var emptyNode = new VNode('', {}, []);
- function getBooleanValue() {
- var val = el.checked;
- if (val && el.hasOwnProperty('_trueValue')) {
- return el._trueValue;
- }
- if (!val && el.hasOwnProperty('_falseValue')) {
- return el._falseValue;
- }
- return val;
- }
+var hooks$1 = ['create', 'update', 'remove', 'destroy'];
- this.listener = function () {
- var model = self._watcher.value;
- if (isArray(model)) {
- var val = self.getValue();
- if (el.checked) {
- if (indexOf(model, val) < 0) {
- model.push(val);
- }
- } else {
- model.$remove(val);
- }
- } else {
- self.set(getBooleanValue());
- }
- };
+function isUndef (s) {
+ return s == null
+}
- this.on('change', this.listener);
- if (el.hasAttribute('checked')) {
- this.afterBind = this.listener;
- }
- },
+function isDef (s) {
+ return s != null
+}
- update: function update(value) {
- var el = this.el;
- if (isArray(value)) {
- el.checked = indexOf(value, this.getValue()) > -1;
- } else {
- if (el.hasOwnProperty('_trueValue')) {
- el.checked = looseEqual(value, el._trueValue);
- } else {
- el.checked = !!value;
- }
- }
- }
- };
+function sameVnode (vnode1, vnode2) {
+ return (
+ vnode1.key === vnode2.key &&
+ vnode1.tag === vnode2.tag &&
+ vnode1.isComment === vnode2.isComment &&
+ !vnode1.data === !vnode2.data
+ )
+}
- var handlers = {
- text: text$2,
- radio: radio,
- select: select,
- checkbox: checkbox
- };
+function createKeyToOldIdx (children, beginIdx, endIdx) {
+ var i, key;
+ var map = {};
+ for (i = beginIdx; i <= endIdx; ++i) {
+ key = children[i].key;
+ if (isDef(key)) { map[key] = i; }
+ }
+ return map
+}
- var model = {
-
- priority: MODEL,
- twoWay: true,
- handlers: handlers,
- params: ['lazy', 'number', 'debounce'],
-
- /**
- * Possible elements:
- * <select>
- * <textarea>
- * <input type="*">
- * - text
- * - checkbox
- * - radio
- * - number
- */
-
- bind: function bind() {
- // friendly warning...
- this.checkFilters();
- if (this.hasRead && !this.hasWrite) {
- 'development' !== 'production' && warn('It seems you are using a read-only filter with ' + 'v-model="' + this.descriptor.raw + '". ' + 'You might want to use a two-way filter to ensure correct behavior.', this.vm);
- }
- var el = this.el;
- var tag = el.tagName;
- var handler;
- if (tag === 'INPUT') {
- handler = handlers[el.type] || handlers.text;
- } else if (tag === 'SELECT') {
- handler = handlers.select;
- } else if (tag === 'TEXTAREA') {
- handler = handlers.text;
- } else {
- 'development' !== 'production' && warn('v-model does not support element type: ' + tag, this.vm);
- return;
- }
- el.__v_model = this;
- handler.bind.call(this);
- this.update = handler.update;
- this._unbind = handler.unbind;
- },
+function createPatchFunction (backend) {
+ var i, j;
+ var cbs = {};
- /**
- * Check read/write filter stats.
- */
-
- checkFilters: function checkFilters() {
- var filters = this.filters;
- if (!filters) return;
- var i = filters.length;
- while (i--) {
- var filter = resolveAsset(this.vm.$options, 'filters', filters[i].name);
- if (typeof filter === 'function' || filter.read) {
- this.hasRead = true;
- }
- if (filter.write) {
- this.hasWrite = true;
- }
- }
- },
+ var modules = backend.modules;
+ var nodeOps = backend.nodeOps;
- unbind: function unbind() {
- this.el.__v_model = null;
- this._unbind && this._unbind();
+ for (i = 0; i < hooks$1.length; ++i) {
+ cbs[hooks$1[i]] = [];
+ for (j = 0; j < modules.length; ++j) {
+ if (modules[j][hooks$1[i]] !== undefined) { cbs[hooks$1[i]].push(modules[j][hooks$1[i]]); }
}
- };
+ }
- // keyCode aliases
- var keyCodes = {
- esc: 27,
- tab: 9,
- enter: 13,
- space: 32,
- 'delete': [8, 46],
- up: 38,
- left: 37,
- right: 39,
- down: 40
- };
+ function emptyNodeAt (elm) {
+ return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
+ }
- function keyFilter(handler, keys) {
- var codes = keys.map(function (key) {
- var charCode = key.charCodeAt(0);
- if (charCode > 47 && charCode < 58) {
- return parseInt(key, 10);
+ function createRmCb (childElm, listeners) {
+ function remove$$1 () {
+ if (--remove$$1.listeners === 0) {
+ removeElement(childElm);
}
- if (key.length === 1) {
- charCode = key.toUpperCase().charCodeAt(0);
- if (charCode > 64 && charCode < 91) {
- return charCode;
- }
- }
- return keyCodes[key];
- });
- codes = [].concat.apply([], codes);
- return function keyHandler(e) {
- if (codes.indexOf(e.keyCode) > -1) {
- return handler.call(this, e);
- }
- };
+ }
+ remove$$1.listeners = listeners;
+ return remove$$1
}
- function stopFilter(handler) {
- return function stopHandler(e) {
- e.stopPropagation();
- return handler.call(this, e);
- };
+ function removeElement (el) {
+ var parent = nodeOps.parentNode(el);
+ nodeOps.removeChild(parent, el);
}
- function preventFilter(handler) {
- return function preventHandler(e) {
- e.preventDefault();
- return handler.call(this, e);
- };
+ function createElm (vnode, insertedVnodeQueue, nested) {
+ var i;
+ var data = vnode.data;
+ vnode.isRootInsert = !nested;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); }
+ // after calling the init hook, if the vnode is a child component
+ // it should've created a child instance and mounted it. the child
+ // component also has set the placeholder vnode's elm.
+ // in that case we can just return the element and be done.
+ if (isDef(i = vnode.child)) {
+ initComponent(vnode, insertedVnodeQueue);
+ return vnode.elm
+ }
+ }
+ var children = vnode.children;
+ var tag = vnode.tag;
+ if (isDef(tag)) {
+ {
+ if (
+ !vnode.ns &&
+ !(config.ignoredElements && config.ignoredElements.indexOf(tag) > -1) &&
+ config.isUnknownElement(tag)
+ ) {
+ warn(
+ 'Unknown custom element: <' + tag + '> - did you ' +
+ 'register the component correctly? For recursive components, ' +
+ 'make sure to provide the "name" option.',
+ vnode.context
+ );
+ }
+ }
+ vnode.elm = vnode.ns
+ ? nodeOps.createElementNS(vnode.ns, tag)
+ : nodeOps.createElement(tag, vnode);
+ setScope(vnode);
+ createChildren(vnode, children, insertedVnodeQueue);
+ if (isDef(data)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
+ }
+ } else if (vnode.isComment) {
+ vnode.elm = nodeOps.createComment(vnode.text);
+ } else {
+ vnode.elm = nodeOps.createTextNode(vnode.text);
+ }
+ return vnode.elm
}
- function selfFilter(handler) {
- return function selfHandler(e) {
- if (e.target === e.currentTarget) {
- return handler.call(this, e);
+ function createChildren (vnode, children, insertedVnodeQueue) {
+ if (Array.isArray(children)) {
+ for (var i = 0; i < children.length; ++i) {
+ nodeOps.appendChild(vnode.elm, createElm(children[i], insertedVnodeQueue, true));
}
- };
+ } else if (isPrimitive(vnode.text)) {
+ nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text));
+ }
}
- var on$1 = {
-
- priority: ON,
- acceptStatement: true,
- keyCodes: keyCodes,
-
- bind: function bind() {
- // deal with iframes
- if (this.el.tagName === 'IFRAME' && this.arg !== 'load') {
- var self = this;
- this.iframeBind = function () {
- on(self.el.contentWindow, self.arg, self.handler, self.modifiers.capture);
- };
- this.on('load', this.iframeBind);
- }
- },
-
- update: function update(handler) {
- // stub a noop for v-on with no value,
- // e.g. @mousedown.prevent
- if (!this.descriptor.raw) {
- handler = function () {};
- }
-
- if (typeof handler !== 'function') {
- 'development' !== 'production' && warn('v-on:' + this.arg + '="' + this.expression + '" expects a function value, ' + 'got ' + handler, this.vm);
- return;
- }
-
- // apply modifiers
- if (this.modifiers.stop) {
- handler = stopFilter(handler);
- }
- if (this.modifiers.prevent) {
- handler = preventFilter(handler);
- }
- if (this.modifiers.self) {
- handler = selfFilter(handler);
- }
- // key filter
- var keys = Object.keys(this.modifiers).filter(function (key) {
- return key !== 'stop' && key !== 'prevent' && key !== 'self' && key !== 'capture';
- });
- if (keys.length) {
- handler = keyFilter(handler, keys);
- }
-
- this.reset();
- this.handler = handler;
-
- if (this.iframeBind) {
- this.iframeBind();
- } else {
- on(this.el, this.arg, this.handler, this.modifiers.capture);
- }
- },
-
- reset: function reset() {
- var el = this.iframeBind ? this.el.contentWindow : this.el;
- if (this.handler) {
- off(el, this.arg, this.handler);
- }
- },
-
- unbind: function unbind() {
- this.reset();
+ function isPatchable (vnode) {
+ while (vnode.child) {
+ vnode = vnode.child._vnode;
}
- };
-
- var prefixes = ['-webkit-', '-moz-', '-ms-'];
- var camelPrefixes = ['Webkit', 'Moz', 'ms'];
- var importantRE = /!important;?$/;
- var propCache = Object.create(null);
-
- var testEl = null;
-
- var style = {
-
- deep: true,
-
- update: function update(value) {
- if (typeof value === 'string') {
- this.el.style.cssText = value;
- } else if (isArray(value)) {
- this.handleObject(value.reduce(extend, {}));
- } else {
- this.handleObject(value || {});
- }
- },
-
- handleObject: function handleObject(value) {
- // cache object styles so that only changed props
- // are actually updated.
- var cache = this.cache || (this.cache = {});
- var name, val;
- for (name in cache) {
- if (!(name in value)) {
- this.handleSingle(name, null);
- delete cache[name];
- }
- }
- for (name in value) {
- val = value[name];
- if (val !== cache[name]) {
- cache[name] = val;
- this.handleSingle(name, val);
- }
- }
- },
+ return isDef(vnode.tag)
+ }
- handleSingle: function handleSingle(prop, value) {
- prop = normalize(prop);
- if (!prop) return; // unsupported prop
- // cast possible numbers/booleans into strings
- if (value != null) value += '';
- if (value) {
- var isImportant = importantRE.test(value) ? 'important' : '';
- if (isImportant) {
- /* istanbul ignore if */
- if ('development' !== 'production') {
- warn('It\'s probably a bad idea to use !important with inline rules. ' + 'This feature will be deprecated in a future version of Vue.');
- }
- value = value.replace(importantRE, '').trim();
- this.el.style.setProperty(prop.kebab, value, isImportant);
- } else {
- this.el.style[prop.camel] = value;
- }
- } else {
- this.el.style[prop.camel] = '';
- }
+ function invokeCreateHooks (vnode, insertedVnodeQueue) {
+ for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
+ cbs.create[i$1](emptyNode, vnode);
}
-
- };
-
- /**
- * Normalize a CSS property name.
- * - cache result
- * - auto prefix
- * - camelCase -> dash-case
- *
- * @param {String} prop
- * @return {String}
- */
-
- function normalize(prop) {
- if (propCache[prop]) {
- return propCache[prop];
+ i = vnode.data.hook; // Reuse variable
+ if (isDef(i)) {
+ if (i.create) { i.create(emptyNode, vnode); }
+ if (i.insert) { insertedVnodeQueue.push(vnode); }
}
- var res = prefix(prop);
- propCache[prop] = propCache[res] = res;
- return res;
}
- /**
- * Auto detect the appropriate prefix for a CSS property.
- * https://gist.github.com/paulirish/523692
- *
- * @param {String} prop
- * @return {String}
- */
-
- function prefix(prop) {
- prop = hyphenate(prop);
- var camel = camelize(prop);
- var upper = camel.charAt(0).toUpperCase() + camel.slice(1);
- if (!testEl) {
- testEl = document.createElement('div');
- }
- var i = prefixes.length;
- var prefixed;
- if (camel !== 'filter' && camel in testEl.style) {
- return {
- kebab: prop,
- camel: camel
- };
+ function initComponent (vnode, insertedVnodeQueue) {
+ if (vnode.data.pendingInsert) {
+ insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
}
- while (i--) {
- prefixed = camelPrefixes[i] + upper;
- if (prefixed in testEl.style) {
- return {
- kebab: prefixes[i] + prop,
- camel: prefixed
- };
- }
- }
- }
-
- // xlink
- var xlinkNS = 'http://www.w3.org/1999/xlink';
- var xlinkRE = /^xlink:/;
-
- // check for attributes that prohibit interpolations
- var disallowedInterpAttrRE = /^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/;
- // these attributes should also set their corresponding properties
- // because they only affect the initial state of the element
- var attrWithPropsRE = /^(?:value|checked|selected|muted)$/;
- // these attributes expect enumrated values of "true" or "false"
- // but are not boolean attributes
- var enumeratedAttrRE = /^(?:draggable|contenteditable|spellcheck)$/;
-
- // these attributes should set a hidden property for
- // binding v-model to object values
- var modelProps = {
- value: '_value',
- 'true-value': '_trueValue',
- 'false-value': '_falseValue'
- };
+ vnode.elm = vnode.child.$el;
+ if (isPatchable(vnode)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
+ setScope(vnode);
+ } else {
+ // empty component root.
+ // skip all element-related modules except for ref (#3455)
+ registerRef(vnode);
+ // make sure to invoke the insert hook
+ insertedVnodeQueue.push(vnode);
+ }
+ }
- var bind$1 = {
+ // set scope id attribute for scoped CSS.
+ // this is implemented as a special case to avoid the overhead
+ // of going through the normal attribute patching process.
+ function setScope (vnode) {
+ var i;
+ if (isDef(i = vnode.context) && isDef(i = i.$options._scopeId)) {
+ nodeOps.setAttribute(vnode.elm, i, '');
+ }
+ if (isDef(i = activeInstance) &&
+ i !== vnode.context &&
+ isDef(i = i.$options._scopeId)) {
+ nodeOps.setAttribute(vnode.elm, i, '');
+ }
+ }
- priority: BIND,
+ function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
+ for (; startIdx <= endIdx; ++startIdx) {
+ nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
+ }
+ }
- bind: function bind() {
- var attr = this.arg;
- var tag = this.el.tagName;
- // should be deep watch on object mode
- if (!attr) {
- this.deep = true;
+ function invokeDestroyHook (vnode) {
+ var i, j;
+ var data = vnode.data;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }
+ for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }
+ }
+ if (isDef(i = vnode.children)) {
+ for (j = 0; j < vnode.children.length; ++j) {
+ invokeDestroyHook(vnode.children[j]);
}
- // handle interpolation bindings
- var descriptor = this.descriptor;
- var tokens = descriptor.interp;
- if (tokens) {
- // handle interpolations with one-time tokens
- if (descriptor.hasOneTime) {
- this.expression = tokensToExp(tokens, this._scope || this.vm);
- }
-
- // only allow binding on native attributes
- if (disallowedInterpAttrRE.test(attr) || attr === 'name' && (tag === 'PARTIAL' || tag === 'SLOT')) {
- 'development' !== 'production' && warn(attr + '="' + descriptor.raw + '": ' + 'attribute interpolation is not allowed in Vue.js ' + 'directives and special attributes.', this.vm);
- this.el.removeAttribute(attr);
- this.invalid = true;
- }
-
- /* istanbul ignore if */
- if ('development' !== 'production') {
- var raw = attr + '="' + descriptor.raw + '": ';
- // warn src
- if (attr === 'src') {
- warn(raw + 'interpolation in "src" attribute will cause ' + 'a 404 request. Use v-bind:src instead.', this.vm);
- }
+ }
+ }
- // warn style
- if (attr === 'style') {
- warn(raw + 'interpolation in "style" attribute will cause ' + 'the attribute to be discarded in Internet Explorer. ' + 'Use v-bind:style instead.', this.vm);
- }
+ function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
+ for (; startIdx <= endIdx; ++startIdx) {
+ var ch = vnodes[startIdx];
+ if (isDef(ch)) {
+ if (isDef(ch.tag)) {
+ removeAndInvokeRemoveHook(ch);
+ invokeDestroyHook(ch);
+ } else { // Text node
+ nodeOps.removeChild(parentElm, ch.elm);
}
}
- },
+ }
+ }
- update: function update(value) {
- if (this.invalid) {
- return;
- }
- var attr = this.arg;
- if (this.arg) {
- this.handleSingle(attr, value);
+ function removeAndInvokeRemoveHook (vnode, rm) {
+ if (rm || isDef(vnode.data)) {
+ var listeners = cbs.remove.length + 1;
+ if (!rm) {
+ // directly removing
+ rm = createRmCb(vnode.elm, listeners);
} else {
- this.handleObject(value || {});
+ // we have a recursively passed down rm callback
+ // increase the listeners count
+ rm.listeners += listeners;
}
- },
-
- // share object handler with v-bind:class
- handleObject: style.handleObject,
-
- handleSingle: function handleSingle(attr, value) {
- var el = this.el;
- var interp = this.descriptor.interp;
- if (this.modifiers.camel) {
- attr = camelize(attr);
+ // recursively invoke hooks on child component root node
+ if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
+ removeAndInvokeRemoveHook(i, rm);
}
- if (!interp && attrWithPropsRE.test(attr) && attr in el) {
- var attrValue = attr === 'value' ? value == null // IE9 will set input.value to "null" for null...
- ? '' : value : value;
-
- if (el[attr] !== attrValue) {
- el[attr] = attrValue;
- }
+ for (i = 0; i < cbs.remove.length; ++i) {
+ cbs.remove[i](vnode, rm);
}
- // set model props
- var modelProp = modelProps[attr];
- if (!interp && modelProp) {
- el[modelProp] = value;
- // update v-model if present
- var model = el.__v_model;
- if (model) {
- model.listener();
- }
+ if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
+ i(vnode, rm);
+ } else {
+ rm();
}
- // do not set value attribute for textarea
- if (attr === 'value' && el.tagName === 'TEXTAREA') {
- el.removeAttribute(attr);
- return;
- }
- // update attribute
- if (enumeratedAttrRE.test(attr)) {
- el.setAttribute(attr, value ? 'true' : 'false');
- } else if (value != null && value !== false) {
- if (attr === 'class') {
- // handle edge case #1960:
- // class interpolation should not overwrite Vue transition class
- if (el.__v_trans) {
- value += ' ' + el.__v_trans.id + '-transition';
- }
- setClass(el, value);
- } else if (xlinkRE.test(attr)) {
- el.setAttributeNS(xlinkNS, attr, value === true ? '' : value);
+ } else {
+ removeElement(vnode.elm);
+ }
+ }
+
+ function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
+ var oldStartIdx = 0;
+ var newStartIdx = 0;
+ var oldEndIdx = oldCh.length - 1;
+ var oldStartVnode = oldCh[0];
+ var oldEndVnode = oldCh[oldEndIdx];
+ var newEndIdx = newCh.length - 1;
+ var newStartVnode = newCh[0];
+ var newEndVnode = newCh[newEndIdx];
+ var oldKeyToIdx, idxInOld, elmToMove, before;
+
+ // removeOnly is a special flag used only by <transition-group>
+ // to ensure removed elements stay in correct relative positions
+ // during leaving transitions
+ var canMove = !removeOnly;
+
+ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
+ if (isUndef(oldStartVnode)) {
+ oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
+ } else if (isUndef(oldEndVnode)) {
+ oldEndVnode = oldCh[--oldEndIdx];
+ } else if (sameVnode(oldStartVnode, newStartVnode)) {
+ patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
+ oldStartVnode = oldCh[++oldStartIdx];
+ newStartVnode = newCh[++newStartIdx];
+ } else if (sameVnode(oldEndVnode, newEndVnode)) {
+ patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
+ oldEndVnode = oldCh[--oldEndIdx];
+ newEndVnode = newCh[--newEndIdx];
+ } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
+ patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
+ canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
+ oldStartVnode = oldCh[++oldStartIdx];
+ newEndVnode = newCh[--newEndIdx];
+ } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
+ patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
+ canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
+ oldEndVnode = oldCh[--oldEndIdx];
+ newStartVnode = newCh[++newStartIdx];
+ } else {
+ if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
+ idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
+ if (isUndef(idxInOld)) { // New element
+ nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
} else {
- el.setAttribute(attr, value === true ? '' : value);
+ elmToMove = oldCh[idxInOld];
+ /* istanbul ignore if */
+ if ("development" !== 'production' && !elmToMove) {
+ warn(
+ 'It seems there are duplicate keys that is causing an update error. ' +
+ 'Make sure each v-for item has a unique key.'
+ );
+ }
+ if (elmToMove.tag !== newStartVnode.tag) {
+ // same key but different element. treat as new element
+ nodeOps.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
+ } else {
+ patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
+ oldCh[idxInOld] = undefined;
+ canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
+ newStartVnode = newCh[++newStartIdx];
+ }
}
- } else {
- el.removeAttribute(attr);
}
}
- };
-
- var el = {
-
- priority: EL,
-
- bind: function bind() {
- /* istanbul ignore if */
- if (!this.arg) {
- return;
- }
- var id = this.id = camelize(this.arg);
- var refs = (this._scope || this.vm).$els;
- if (hasOwn(refs, id)) {
- refs[id] = this.el;
- } else {
- defineReactive(refs, id, this.el);
- }
- },
-
- unbind: function unbind() {
- var refs = (this._scope || this.vm).$els;
- if (refs[this.id] === this.el) {
- refs[this.id] = null;
- }
+ if (oldStartIdx > oldEndIdx) {
+ before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
+ addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
+ } else if (newStartIdx > newEndIdx) {
+ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
- };
+ }
- var ref = {
- bind: function bind() {
- 'development' !== 'production' && warn('v-ref:' + this.arg + ' must be used on a child ' + 'component. Found on <' + this.el.tagName.toLowerCase() + '>.', this.vm);
+ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
+ if (oldVnode === vnode) {
+ return
}
- };
-
- var cloak = {
- bind: function bind() {
- var el = this.el;
- this.vm.$once('pre-hook:compiled', function () {
- el.removeAttribute('v-cloak');
- });
+ // reuse element for static trees.
+ // note we only do this if the vnode is cloned -
+ // if the new node is not cloned it means the render functions have been
+ // reset by the hot-reload-api and we need to do a proper re-render.
+ if (vnode.isStatic &&
+ oldVnode.isStatic &&
+ vnode.key === oldVnode.key &&
+ vnode.isCloned) {
+ vnode.elm = oldVnode.elm;
+ return
}
- };
-
- // must export plain object
- var directives = {
- text: text$1,
- html: html,
- 'for': vFor,
- 'if': vIf,
- show: show,
- model: model,
- on: on$1,
- bind: bind$1,
- el: el,
- ref: ref,
- cloak: cloak
- };
-
- var vClass = {
-
- deep: true,
-
- update: function update(value) {
- if (!value) {
- this.cleanup();
- } else if (typeof value === 'string') {
- this.setClass(value.trim().split(/\s+/));
- } else {
- this.setClass(normalize$1(value));
+ var i;
+ var data = vnode.data;
+ var hasData = isDef(data);
+ if (hasData && isDef(i = data.hook) && isDef(i = i.prepatch)) {
+ i(oldVnode, vnode);
+ }
+ var elm = vnode.elm = oldVnode.elm;
+ var oldCh = oldVnode.children;
+ var ch = vnode.children;
+ if (hasData && isPatchable(vnode)) {
+ for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
+ if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
+ }
+ if (isUndef(vnode.text)) {
+ if (isDef(oldCh) && isDef(ch)) {
+ if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
+ } else if (isDef(ch)) {
+ if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
+ addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
+ } else if (isDef(oldCh)) {
+ removeVnodes(elm, oldCh, 0, oldCh.length - 1);
+ } else if (isDef(oldVnode.text)) {
+ nodeOps.setTextContent(elm, '');
+ }
+ } else if (oldVnode.text !== vnode.text) {
+ nodeOps.setTextContent(elm, vnode.text);
+ }
+ if (hasData) {
+ if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
+ }
+ }
+
+ function invokeInsertHook (vnode, queue, initial) {
+ // delay insert hooks for component root nodes, invoke them after the
+ // element is really inserted
+ if (initial && vnode.parent) {
+ vnode.parent.data.pendingInsert = queue;
+ } else {
+ for (var i = 0; i < queue.length; ++i) {
+ queue[i].data.hook.insert(queue[i]);
}
- },
+ }
+ }
- setClass: function setClass(value) {
- this.cleanup(value);
- for (var i = 0, l = value.length; i < l; i++) {
- var val = value[i];
- if (val) {
- apply(this.el, val, addClass);
- }
+ var bailed = false;
+ function hydrate (elm, vnode, insertedVnodeQueue) {
+ {
+ if (!assertNodeMatch(elm, vnode)) {
+ return false
}
- this.prevKeys = value;
- },
-
- cleanup: function cleanup(value) {
- var prevKeys = this.prevKeys;
- if (!prevKeys) return;
- var i = prevKeys.length;
- while (i--) {
- var key = prevKeys[i];
- if (!value || value.indexOf(key) < 0) {
- apply(this.el, key, removeClass);
- }
+ }
+ vnode.elm = elm;
+ var tag = vnode.tag;
+ var data = vnode.data;
+ var children = vnode.children;
+ if (isDef(data)) {
+ if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode, true /* hydrating */); }
+ if (isDef(i = vnode.child)) {
+ // child component. it should have hydrated its own tree.
+ initComponent(vnode, insertedVnodeQueue);
+ return true
}
}
- };
-
- /**
- * Normalize objects and arrays (potentially containing objects)
- * into array of strings.
- *
- * @param {Object|Array<String|Object>} value
- * @return {Array<String>}
- */
-
- function normalize$1(value) {
- var res = [];
- if (isArray(value)) {
- for (var i = 0, l = value.length; i < l; i++) {
- var _key = value[i];
- if (_key) {
- if (typeof _key === 'string') {
- res.push(_key);
+ if (isDef(tag)) {
+ if (isDef(children)) {
+ var childNodes = nodeOps.childNodes(elm);
+ // empty element, allow client to pick up and populate children
+ if (!childNodes.length) {
+ createChildren(vnode, children, insertedVnodeQueue);
+ } else {
+ var childrenMatch = true;
+ if (childNodes.length !== children.length) {
+ childrenMatch = false;
} else {
- for (var k in _key) {
- if (_key[k]) res.push(k);
+ for (var i$1 = 0; i$1 < children.length; i$1++) {
+ if (!hydrate(childNodes[i$1], children[i$1], insertedVnodeQueue)) {
+ childrenMatch = false;
+ break
+ }
}
}
+ if (!childrenMatch) {
+ if ("development" !== 'production' &&
+ typeof console !== 'undefined' &&
+ !bailed) {
+ bailed = true;
+ console.warn('Parent: ', elm);
+ console.warn('Mismatching childNodes vs. VNodes: ', childNodes, children);
+ }
+ return false
+ }
}
}
- } else if (isObject(value)) {
- for (var key in value) {
- if (value[key]) res.push(key);
+ if (isDef(data)) {
+ invokeCreateHooks(vnode, insertedVnodeQueue);
}
}
- return res;
+ return true
}
- /**
- * Add or remove a class/classes on an element
- *
- * @param {Element} el
- * @param {String} key The class name. This may or may not
- * contain a space character, in such a
- * case we'll deal with multiple class
- * names at once.
- * @param {Function} fn
- */
-
- function apply(el, key, fn) {
- key = key.trim();
- if (key.indexOf(' ') === -1) {
- fn(el, key);
- return;
- }
- // The key contains one or more space characters.
- // Since a class name doesn't accept such characters, we
- // treat it as multiple classes.
- var keys = key.split(/\s+/);
- for (var i = 0, l = keys.length; i < l; i++) {
- fn(el, keys[i]);
+ function assertNodeMatch (node, vnode) {
+ if (vnode.tag) {
+ return (
+ vnode.tag.indexOf('vue-component') === 0 ||
+ vnode.tag === nodeOps.tagName(node).toLowerCase()
+ )
+ } else {
+ return _toString(vnode.text) === node.data
}
}
- var component = {
-
- priority: COMPONENT,
-
- params: ['keep-alive', 'transition-mode', 'inline-template'],
-
- /**
- * Setup. Two possible usages:
- *
- * - static:
- * <comp> or <div v-component="comp">
- *
- * - dynamic:
- * <component :is="view">
- */
-
- bind: function bind() {
- if (!this.el.__vue__) {
- // keep-alive cache
- this.keepAlive = this.params.keepAlive;
- if (this.keepAlive) {
- this.cache = {};
- }
- // check inline-template
- if (this.params.inlineTemplate) {
- // extract inline template as a DocumentFragment
- this.inlineTemplate = extractContent(this.el, true);
- }
- // component resolution related state
- this.pendingComponentCb = this.Component = null;
- // transition related state
- this.pendingRemovals = 0;
- this.pendingRemovalCb = null;
- // create a ref anchor
- this.anchor = createAnchor('v-component');
- replace(this.el, this.anchor);
- // remove is attribute.
- // this is removed during compilation, but because compilation is
- // cached, when the component is used elsewhere this attribute
- // will remain at link time.
- this.el.removeAttribute('is');
- this.el.removeAttribute(':is');
- // remove ref, same as above
- if (this.descriptor.ref) {
- this.el.removeAttribute('v-ref:' + hyphenate(this.descriptor.ref));
- }
- // if static, build right now.
- if (this.literal) {
- this.setComponent(this.expression);
- }
- } else {
- 'development' !== 'production' && warn('cannot mount component "' + this.expression + '" ' + 'on already mounted element: ' + this.el);
- }
- },
-
- /**
- * Public update, called by the watcher in the dynamic
- * literal scenario, e.g. <component :is="view">
- */
+ return function patch (oldVnode, vnode, hydrating, removeOnly) {
+ if (!vnode) {
+ if (oldVnode) { invokeDestroyHook(oldVnode); }
+ return
+ }
- update: function update(value) {
- if (!this.literal) {
- this.setComponent(value);
- }
- },
+ var elm, parent;
+ var isInitialPatch = false;
+ var insertedVnodeQueue = [];
- /**
- * Switch dynamic components. May resolve the component
- * asynchronously, and perform transition based on
- * specified transition mode. Accepts a few additional
- * arguments specifically for vue-router.
- *
- * The callback is called when the full transition is
- * finished.
- *
- * @param {String} value
- * @param {Function} [cb]
- */
-
- setComponent: function setComponent(value, cb) {
- this.invalidatePending();
- if (!value) {
- // just remove current
- this.unbuild(true);
- this.remove(this.childVM, cb);
- this.childVM = null;
+ if (!oldVnode) {
+ // empty mount, create new root element
+ isInitialPatch = true;
+ createElm(vnode, insertedVnodeQueue);
+ } else {
+ var isRealElement = isDef(oldVnode.nodeType);
+ if (!isRealElement && sameVnode(oldVnode, vnode)) {
+ patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
- var self = this;
- this.resolveComponent(value, function () {
- self.mountComponent(cb);
- });
- }
- },
-
- /**
- * Resolve the component constructor to use when creating
- * the child vm.
- *
- * @param {String|Function} value
- * @param {Function} cb
- */
-
- resolveComponent: function resolveComponent(value, cb) {
- var self = this;
- this.pendingComponentCb = cancellable(function (Component) {
- self.ComponentName = Component.options.name || (typeof value === 'string' ? value : null);
- self.Component = Component;
- cb();
- });
- this.vm._resolveComponent(value, this.pendingComponentCb);
- },
-
- /**
- * Create a new instance using the current constructor and
- * replace the existing instance. This method doesn't care
- * whether the new component and the old one are actually
- * the same.
- *
- * @param {Function} [cb]
- */
-
- mountComponent: function mountComponent(cb) {
- // actual mount
- this.unbuild(true);
- var self = this;
- var activateHooks = this.Component.options.activate;
- var cached = this.getCached();
- var newComponent = this.build();
- if (activateHooks && !cached) {
- this.waitingFor = newComponent;
- callActivateHooks(activateHooks, newComponent, function () {
- if (self.waitingFor !== newComponent) {
- return;
+ if (isRealElement) {
+ // mounting to a real element
+ // check if this is server-rendered content and if we can perform
+ // a successful hydration.
+ if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) {
+ oldVnode.removeAttribute('server-rendered');
+ hydrating = true;
}
- self.waitingFor = null;
- self.transition(newComponent, cb);
- });
- } else {
- // update ref for kept-alive component
- if (cached) {
- newComponent._updateRef();
- }
- this.transition(newComponent, cb);
- }
- },
-
- /**
- * When the component changes or unbinds before an async
- * constructor is resolved, we need to invalidate its
- * pending callback.
- */
-
- invalidatePending: function invalidatePending() {
- if (this.pendingComponentCb) {
- this.pendingComponentCb.cancel();
- this.pendingComponentCb = null;
- }
- },
-
- /**
- * Instantiate/insert a new child vm.
- * If keep alive and has cached instance, insert that
- * instance; otherwise build a new one and cache it.
- *
- * @param {Object} [extraOptions]
- * @return {Vue} - the created instance
- */
-
- build: function build(extraOptions) {
- var cached = this.getCached();
- if (cached) {
- return cached;
- }
- if (this.Component) {
- // default options
- var options = {
- name: this.ComponentName,
- el: cloneNode(this.el),
- template: this.inlineTemplate,
- // make sure to add the child with correct parent
- // if this is a transcluded component, its parent
- // should be the transclusion host.
- parent: this._host || this.vm,
- // if no inline-template, then the compiled
- // linker can be cached for better performance.
- _linkerCachable: !this.inlineTemplate,
- _ref: this.descriptor.ref,
- _asComponent: true,
- _isRouterView: this._isRouterView,
- // if this is a transcluded component, context
- // will be the common parent vm of this instance
- // and its host.
- _context: this.vm,
- // if this is inside an inline v-for, the scope
- // will be the intermediate scope created for this
- // repeat fragment. this is used for linking props
- // and container directives.
- _scope: this._scope,
- // pass in the owner fragment of this component.
- // this is necessary so that the fragment can keep
- // track of its contained components in order to
- // call attach/detach hooks for them.
- _frag: this._frag
- };
- // extra options
- // in 1.0.0 this is used by vue-router only
- /* istanbul ignore if */
- if (extraOptions) {
- extend(options, extraOptions);
- }
- var child = new this.Component(options);
- if (this.keepAlive) {
- this.cache[this.Component.cid] = child;
- }
- /* istanbul ignore if */
- if ('development' !== 'production' && this.el.hasAttribute('transition') && child._isFragment) {
- warn('Transitions will not work on a fragment instance. ' + 'Template: ' + child.$options.template, child);
- }
- return child;
- }
- },
-
- /**
- * Try to get a cached instance of the current component.
- *
- * @return {Vue|undefined}
- */
-
- getCached: function getCached() {
- return this.keepAlive && this.cache[this.Component.cid];
- },
-
- /**
- * Teardown the current child, but defers cleanup so
- * that we can separate the destroy and removal steps.
- *
- * @param {Boolean} defer
- */
-
- unbuild: function unbuild(defer) {
- if (this.waitingFor) {
- if (!this.keepAlive) {
- this.waitingFor.$destroy();
- }
- this.waitingFor = null;
- }
- var child = this.childVM;
- if (!child || this.keepAlive) {
- if (child) {
- // remove ref
- child._inactive = true;
- child._updateRef(true);
- }
- return;
- }
- // the sole purpose of `deferCleanup` is so that we can
- // "deactivate" the vm right now and perform DOM removal
- // later.
- child.$destroy(false, defer);
- },
-
- /**
- * Remove current destroyed child and manually do
- * the cleanup after removal.
- *
- * @param {Function} cb
- */
-
- remove: function remove(child, cb) {
- var keepAlive = this.keepAlive;
- if (child) {
- // we may have a component switch when a previous
- // component is still being transitioned out.
- // we want to trigger only one lastest insertion cb
- // when the existing transition finishes. (#1119)
- this.pendingRemovals++;
- this.pendingRemovalCb = cb;
- var self = this;
- child.$remove(function () {
- self.pendingRemovals--;
- if (!keepAlive) child._cleanup();
- if (!self.pendingRemovals && self.pendingRemovalCb) {
- self.pendingRemovalCb();
- self.pendingRemovalCb = null;
+ if (hydrating) {
+ if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
+ invokeInsertHook(vnode, insertedVnodeQueue, true);
+ return oldVnode
+ } else {
+ warn(
+ 'The client-side rendered virtual DOM tree is not matching ' +
+ 'server-rendered content. This is likely caused by incorrect ' +
+ 'HTML markup, for example nesting block-level elements inside ' +
+ '<p>, or missing <tbody>. Bailing hydration and performing ' +
+ 'full client-side render.'
+ );
+ }
}
- });
- } else if (cb) {
- cb();
- }
- },
-
- /**
- * Actually swap the components, depending on the
- * transition mode. Defaults to simultaneous.
- *
- * @param {Vue} target
- * @param {Function} [cb]
- */
-
- transition: function transition(target, cb) {
- var self = this;
- var current = this.childVM;
- // for devtool inspection
- if (current) current._inactive = true;
- target._inactive = false;
- this.childVM = target;
- switch (self.params.transitionMode) {
- case 'in-out':
- target.$before(self.anchor, function () {
- self.remove(current, cb);
- });
- break;
- case 'out-in':
- self.remove(current, function () {
- target.$before(self.anchor, cb);
- });
- break;
- default:
- self.remove(current);
- target.$before(self.anchor, cb);
- }
- },
+ // either not server-rendered, or hydration failed.
+ // create an empty node and replace it
+ oldVnode = emptyNodeAt(oldVnode);
+ }
+ elm = oldVnode.elm;
+ parent = nodeOps.parentNode(elm);
+
+ createElm(vnode, insertedVnodeQueue);
+
+ // component root element replaced.
+ // update parent placeholder node element.
+ if (vnode.parent) {
+ vnode.parent.elm = vnode.elm;
+ if (isPatchable(vnode)) {
+ for (var i = 0; i < cbs.create.length; ++i) {
+ cbs.create[i](emptyNode, vnode.parent);
+ }
+ }
+ }
- /**
- * Unbind.
- */
-
- unbind: function unbind() {
- this.invalidatePending();
- // Do not defer cleanup when unbinding
- this.unbuild();
- // destroy all keep-alive cached instances
- if (this.cache) {
- for (var key in this.cache) {
- this.cache[key].$destroy();
+ if (parent !== null) {
+ nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm));
+ removeVnodes(parent, [oldVnode], 0, 0);
+ } else if (isDef(oldVnode.tag)) {
+ invokeDestroyHook(oldVnode);
}
- this.cache = null;
}
}
- };
-
- /**
- * Call activate hooks in order (asynchronous)
- *
- * @param {Array} hooks
- * @param {Vue} vm
- * @param {Function} cb
- */
- function callActivateHooks(hooks, vm, cb) {
- var total = hooks.length;
- var called = 0;
- hooks[0].call(vm, next);
- function next() {
- if (++called >= total) {
- cb();
- } else {
- hooks[called].call(vm, next);
- }
- }
+ invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
+ return vnode.elm
}
+}
- var propBindingModes = config._propBindingModes;
- var empty = {};
-
- // regexes
- var identRE$1 = /^[$_a-zA-Z]+[\w$]*$/;
- var settablePathRE = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[[^\[\]]+\])*$/;
-
- /**
- * Compile props on a root element and return
- * a props link function.
- *
- * @param {Element|DocumentFragment} el
- * @param {Array} propOptions
- * @param {Vue} vm
- * @return {Function} propsLinkFn
- */
-
- function compileProps(el, propOptions, vm) {
- var props = [];
- var names = Object.keys(propOptions);
- var i = names.length;
- var options, name, attr, value, path, parsed, prop;
- while (i--) {
- name = names[i];
- options = propOptions[name] || empty;
+/* */
- if ('development' !== 'production' && name === '$data') {
- warn('Do not use $data as prop.', vm);
- continue;
- }
+var directives = {
+ create: updateDirectives,
+ update: updateDirectives,
+ destroy: function unbindDirectives (vnode) {
+ updateDirectives(vnode, emptyNode);
+ }
+};
- // props could contain dashes, which will be
- // interpreted as minus calculations by the parser
- // so we need to camelize the path here
- path = camelize(name);
- if (!identRE$1.test(path)) {
- 'development' !== 'production' && warn('Invalid prop key: "' + name + '". Prop keys ' + 'must be valid identifiers.', vm);
- continue;
- }
+function updateDirectives (
+ oldVnode,
+ vnode
+) {
+ if (!oldVnode.data.directives && !vnode.data.directives) {
+ return
+ }
+ var isCreate = oldVnode === emptyNode;
+ var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
+ var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
- prop = {
- name: name,
- path: path,
- options: options,
- mode: propBindingModes.ONE_WAY,
- raw: null
- };
+ var dirsWithInsert = [];
+ var dirsWithPostpatch = [];
- attr = hyphenate(name);
- // first check dynamic version
- if ((value = getBindAttr(el, attr)) === null) {
- if ((value = getBindAttr(el, attr + '.sync')) !== null) {
- prop.mode = propBindingModes.TWO_WAY;
- } else if ((value = getBindAttr(el, attr + '.once')) !== null) {
- prop.mode = propBindingModes.ONE_TIME;
- }
+ var key, oldDir, dir;
+ for (key in newDirs) {
+ oldDir = oldDirs[key];
+ dir = newDirs[key];
+ if (!oldDir) {
+ // new directive, bind
+ callHook$1(dir, 'bind', vnode, oldVnode);
+ if (dir.def && dir.def.inserted) {
+ dirsWithInsert.push(dir);
}
- if (value !== null) {
- // has dynamic binding!
- prop.raw = value;
- parsed = parseDirective(value);
- value = parsed.expression;
- prop.filters = parsed.filters;
- // check binding type
- if (isLiteral(value) && !parsed.filters) {
- // for expressions containing literal numbers and
- // booleans, there's no need to setup a prop binding,
- // so we can optimize them as a one-time set.
- prop.optimizedLiteral = true;
- } else {
- prop.dynamic = true;
- // check non-settable path for two-way bindings
- if ('development' !== 'production' && prop.mode === propBindingModes.TWO_WAY && !settablePathRE.test(value)) {
- prop.mode = propBindingModes.ONE_WAY;
- warn('Cannot bind two-way prop with non-settable ' + 'parent path: ' + value, vm);
- }
- }
- prop.parentPath = value;
-
- // warn required two-way
- if ('development' !== 'production' && options.twoWay && prop.mode !== propBindingModes.TWO_WAY) {
- warn('Prop "' + name + '" expects a two-way binding type.', vm);
- }
- } else if ((value = getAttr(el, attr)) !== null) {
- // has literal binding!
- prop.raw = value;
- } else if ('development' !== 'production') {
- // check possible camelCase prop usage
- var lowerCaseName = path.toLowerCase();
- value = /[A-Z\-]/.test(name) && (el.getAttribute(lowerCaseName) || el.getAttribute(':' + lowerCaseName) || el.getAttribute('v-bind:' + lowerCaseName) || el.getAttribute(':' + lowerCaseName + '.once') || el.getAttribute('v-bind:' + lowerCaseName + '.once') || el.getAttribute(':' + lowerCaseName + '.sync') || el.getAttribute('v-bind:' + lowerCaseName + '.sync'));
- if (value) {
- warn('Possible usage error for prop `' + lowerCaseName + '` - ' + 'did you mean `' + attr + '`? HTML is case-insensitive, remember to use ' + 'kebab-case for props in templates.', vm);
- } else if (options.required) {
- // warn missing required
- warn('Missing required prop: ' + name, vm);
- }
+ } else {
+ // existing directive, update
+ dir.oldValue = oldDir.value;
+ callHook$1(dir, 'update', vnode, oldVnode);
+ if (dir.def && dir.def.componentUpdated) {
+ dirsWithPostpatch.push(dir);
}
- // push prop
- props.push(prop);
}
- return makePropsLinkFn(props);
}
- /**
- * Build a function that applies props to a vm.
- *
- * @param {Array} props
- * @return {Function} propsLinkFn
- */
-
- function makePropsLinkFn(props) {
- return function propsLinkFn(vm, scope) {
- // store resolved props info
- vm._props = {};
- var inlineProps = vm.$options.propsData;
- var i = props.length;
- var prop, path, options, value, raw;
- while (i--) {
- prop = props[i];
- raw = prop.raw;
- path = prop.path;
- options = prop.options;
- vm._props[path] = prop;
- if (inlineProps && hasOwn(inlineProps, path)) {
- initProp(vm, prop, inlineProps[path]);
- }if (raw === null) {
- // initialize absent prop
- initProp(vm, prop, undefined);
- } else if (prop.dynamic) {
- // dynamic prop
- if (prop.mode === propBindingModes.ONE_TIME) {
- // one time binding
- value = (scope || vm._context || vm).$get(prop.parentPath);
- initProp(vm, prop, value);
- } else {
- if (vm._context) {
- // dynamic binding
- vm._bindDir({
- name: 'prop',
- def: propDef,
- prop: prop
- }, null, null, scope); // el, host, scope
- } else {
- // root instance
- initProp(vm, prop, vm.$get(prop.parentPath));
- }
- }
- } else if (prop.optimizedLiteral) {
- // optimized literal, cast it and just set once
- var stripped = stripQuotes(raw);
- value = stripped === raw ? toBoolean(toNumber(raw)) : stripped;
- initProp(vm, prop, value);
- } else {
- // string literal, but we need to cater for
- // Boolean props with no value, or with same
- // literal value (e.g. disabled="disabled")
- // see https://github.com/vuejs/vue-loader/issues/182
- value = options.type === Boolean && (raw === '' || raw === hyphenate(prop.name)) ? true : raw;
- initProp(vm, prop, value);
- }
- }
+ if (dirsWithInsert.length) {
+ var callInsert = function () {
+ dirsWithInsert.forEach(function (dir) {
+ callHook$1(dir, 'inserted', vnode, oldVnode);
+ });
};
+ if (isCreate) {
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert, 'dir-insert');
+ } else {
+ callInsert();
+ }
}
- /**
- * Process a prop with a rawValue, applying necessary coersions,
- * default values & assertions and call the given callback with
- * processed value.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} rawValue
- * @param {Function} fn
- */
-
- function processPropValue(vm, prop, rawValue, fn) {
- var isSimple = prop.dynamic && isSimplePath(prop.parentPath);
- var value = rawValue;
- if (value === undefined) {
- value = getPropDefaultValue(vm, prop);
- }
- value = coerceProp(prop, value, vm);
- var coerced = value !== rawValue;
- if (!assertProp(prop, value, vm)) {
- value = undefined;
- }
- if (isSimple && !coerced) {
- withoutConversion(function () {
- fn(value);
+ if (dirsWithPostpatch.length) {
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', function () {
+ dirsWithPostpatch.forEach(function (dir) {
+ callHook$1(dir, 'componentUpdated', vnode, oldVnode);
});
- } else {
- fn(value);
+ }, 'dir-postpatch');
+ }
+
+ if (!isCreate) {
+ for (key in oldDirs) {
+ if (!newDirs[key]) {
+ // no longer present, unbind
+ callHook$1(oldDirs[key], 'unbind', oldVnode);
+ }
}
}
+}
- /**
- * Set a prop's initial value on a vm and its data object.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} value
- */
+var emptyModifiers = Object.create(null);
- function initProp(vm, prop, value) {
- processPropValue(vm, prop, value, function (value) {
- defineReactive(vm, prop.path, value);
- });
+function normalizeDirectives$1 (
+ dirs,
+ vm
+) {
+ var res = Object.create(null);
+ if (!dirs) {
+ return res
}
+ var i, dir;
+ for (i = 0; i < dirs.length; i++) {
+ dir = dirs[i];
+ if (!dir.modifiers) {
+ dir.modifiers = emptyModifiers;
+ }
+ res[getRawDirName(dir)] = dir;
+ dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);
+ }
+ return res
+}
- /**
- * Update a prop's value on a vm.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @param {*} value
- */
+function getRawDirName (dir) {
+ return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
+}
- function updateProp(vm, prop, value) {
- processPropValue(vm, prop, value, function (value) {
- vm[prop.path] = value;
- });
+function callHook$1 (dir, hook, vnode, oldVnode) {
+ var fn = dir.def && dir.def[hook];
+ if (fn) {
+ fn(vnode.elm, dir, vnode, oldVnode);
}
+}
- /**
- * Get the default value of a prop.
- *
- * @param {Vue} vm
- * @param {Object} prop
- * @return {*}
- */
+var baseModules = [
+ ref,
+ directives
+];
- function getPropDefaultValue(vm, prop) {
- // no default, return undefined
- var options = prop.options;
- if (!hasOwn(options, 'default')) {
- // absent boolean value defaults to false
- return options.type === Boolean ? false : undefined;
- }
- var def = options['default'];
- // warn against non-factory defaults for Object & Array
- if (isObject(def)) {
- 'development' !== 'production' && warn('Invalid default value for prop "' + prop.name + '": ' + 'Props with type Object/Array must use a factory function ' + 'to return the default value.', vm);
- }
- // call factory function for non-Function types
- return typeof def === 'function' && options.type !== Function ? def.call(vm) : def;
- }
+/* */
- /**
- * Assert whether a prop is valid.
- *
- * @param {Object} prop
- * @param {*} value
- * @param {Vue} vm
- */
+function updateAttrs (oldVnode, vnode) {
+ if (!oldVnode.data.attrs && !vnode.data.attrs) {
+ return
+ }
+ var key, cur, old;
+ var elm = vnode.elm;
+ var oldAttrs = oldVnode.data.attrs || {};
+ var attrs = vnode.data.attrs || {};
+ // clone observed objects, as the user probably wants to mutate it
+ if (attrs.__ob__) {
+ attrs = vnode.data.attrs = extend({}, attrs);
+ }
- function assertProp(prop, value, vm) {
- if (!prop.options.required && ( // non-required
- prop.raw === null || // abscent
- value == null) // null or undefined
- ) {
- return true;
- }
- var options = prop.options;
- var type = options.type;
- var valid = !type;
- var expectedTypes = [];
- if (type) {
- if (!isArray(type)) {
- type = [type];
- }
- for (var i = 0; i < type.length && !valid; i++) {
- var assertedType = assertType(value, type[i]);
- expectedTypes.push(assertedType.expectedType);
- valid = assertedType.valid;
- }
+ for (key in attrs) {
+ cur = attrs[key];
+ old = oldAttrs[key];
+ if (old !== cur) {
+ setAttr(elm, key, cur);
}
- if (!valid) {
- if ('development' !== 'production') {
- warn('Invalid prop: type check failed for prop "' + prop.name + '".' + ' Expected ' + expectedTypes.map(formatType).join(', ') + ', got ' + formatValue(value) + '.', vm);
- }
- return false;
- }
- var validator = options.validator;
- if (validator) {
- if (!validator(value)) {
- 'development' !== 'production' && warn('Invalid prop: custom validator check failed for prop "' + prop.name + '".', vm);
- return false;
+ }
+ for (key in oldAttrs) {
+ if (attrs[key] == null) {
+ if (isXlink(key)) {
+ elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
+ } else if (!isEnumeratedAttr(key)) {
+ elm.removeAttribute(key);
}
}
- return true;
}
+}
- /**
- * Force parsing value with coerce option.
- *
- * @param {*} value
- * @param {Object} options
- * @return {*}
- */
-
- function coerceProp(prop, value, vm) {
- var coerce = prop.options.coerce;
- if (!coerce) {
- return value;
+function setAttr (el, key, value) {
+ if (isBooleanAttr(key)) {
+ // set attribute for blank value
+ // e.g. <option disabled>Select one</option>
+ if (isFalsyAttrValue(value)) {
+ el.removeAttribute(key);
+ } else {
+ el.setAttribute(key, key);
}
- if (typeof coerce === 'function') {
- return coerce(value);
+ } else if (isEnumeratedAttr(key)) {
+ el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true');
+ } else if (isXlink(key)) {
+ if (isFalsyAttrValue(value)) {
+ el.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else {
- 'development' !== 'production' && warn('Invalid coerce for prop "' + prop.name + '": expected function, got ' + typeof coerce + '.', vm);
- return value;
+ el.setAttributeNS(xlinkNS, key, value);
}
- }
-
- /**
- * Assert the type of a value
- *
- * @param {*} value
- * @param {Function} type
- * @return {Object}
- */
-
- function assertType(value, type) {
- var valid;
- var expectedType;
- if (type === String) {
- expectedType = 'string';
- valid = typeof value === expectedType;
- } else if (type === Number) {
- expectedType = 'number';
- valid = typeof value === expectedType;
- } else if (type === Boolean) {
- expectedType = 'boolean';
- valid = typeof value === expectedType;
- } else if (type === Function) {
- expectedType = 'function';
- valid = typeof value === expectedType;
- } else if (type === Object) {
- expectedType = 'object';
- valid = isPlainObject(value);
- } else if (type === Array) {
- expectedType = 'array';
- valid = isArray(value);
+ } else {
+ if (isFalsyAttrValue(value)) {
+ el.removeAttribute(key);
} else {
- valid = value instanceof type;
+ el.setAttribute(key, value);
}
- return {
- valid: valid,
- expectedType: expectedType
- };
}
+}
- /**
- * Format type for output
- *
- * @param {String} type
- * @return {String}
- */
-
- function formatType(type) {
- return type ? type.charAt(0).toUpperCase() + type.slice(1) : 'custom type';
- }
+var attrs = {
+ create: updateAttrs,
+ update: updateAttrs
+};
- /**
- * Format value
- *
- * @param {*} value
- * @return {String}
- */
+/* */
- function formatValue(val) {
- return Object.prototype.toString.call(val).slice(8, -1);
+function updateClass (oldVnode, vnode) {
+ var el = vnode.elm;
+ var data = vnode.data;
+ var oldData = oldVnode.data;
+ if (!data.staticClass && !data.class &&
+ (!oldData || (!oldData.staticClass && !oldData.class))) {
+ return
}
- var bindingModes = config._propBindingModes;
-
- var propDef = {
+ var cls = genClassForVnode(vnode);
- bind: function bind() {
- var child = this.vm;
- var parent = child._context;
- // passed in from compiler directly
- var prop = this.descriptor.prop;
- var childKey = prop.path;
- var parentKey = prop.parentPath;
- var twoWay = prop.mode === bindingModes.TWO_WAY;
-
- var parentWatcher = this.parentWatcher = new Watcher(parent, parentKey, function (val) {
- updateProp(child, prop, val);
- }, {
- twoWay: twoWay,
- filters: prop.filters,
- // important: props need to be observed on the
- // v-for scope if present
- scope: this._scope
- });
-
- // set the child initial value.
- initProp(child, prop, parentWatcher.value);
-
- // setup two-way binding
- if (twoWay) {
- // important: defer the child watcher creation until
- // the created hook (after data observation)
- var self = this;
- child.$once('pre-hook:created', function () {
- self.childWatcher = new Watcher(child, childKey, function (val) {
- parentWatcher.set(val);
- }, {
- // ensure sync upward before parent sync down.
- // this is necessary in cases e.g. the child
- // mutates a prop array, then replaces it. (#1683)
- sync: true
- });
- });
- }
- },
-
- unbind: function unbind() {
- this.parentWatcher.teardown();
- if (this.childWatcher) {
- this.childWatcher.teardown();
- }
- }
- };
-
- var queue$1 = [];
- var queued = false;
-
- /**
- * Push a job into the queue.
- *
- * @param {Function} job
- */
-
- function pushJob(job) {
- queue$1.push(job);
- if (!queued) {
- queued = true;
- nextTick(flush);
- }
+ // handle transition classes
+ var transitionClass = el._transitionClasses;
+ if (transitionClass) {
+ cls = concat(cls, stringifyClass(transitionClass));
}
- /**
- * Flush the queue, and do one forced reflow before
- * triggering transitions.
- */
-
- function flush() {
- // Force layout
- var f = document.documentElement.offsetHeight;
- for (var i = 0; i < queue$1.length; i++) {
- queue$1[i]();
- }
- queue$1 = [];
- queued = false;
- // dummy return, so js linters don't complain about
- // unused variable f
- return f;
+ // set the class
+ if (cls !== el._prevClass) {
+ el.setAttribute('class', cls);
+ el._prevClass = cls;
}
+}
- var TYPE_TRANSITION = 'transition';
- var TYPE_ANIMATION = 'animation';
- var transDurationProp = transitionProp + 'Duration';
- var animDurationProp = animationProp + 'Duration';
+var klass = {
+ create: updateClass,
+ update: updateClass
+};
- /**
- * If a just-entered element is applied the
- * leave class while its enter transition hasn't started yet,
- * and the transitioned property has the same value for both
- * enter/leave, then the leave transition will be skipped and
- * the transitionend event never fires. This function ensures
- * its callback to be called after a transition has started
- * by waiting for double raf.
- *
- * It falls back to setTimeout on devices that support CSS
- * transitions but not raf (e.g. Android 4.2 browser) - since
- * these environments are usually slow, we are giving it a
- * relatively large timeout.
- */
-
- var raf = inBrowser && window.requestAnimationFrame;
- var waitForTransitionStart = raf
- /* istanbul ignore next */
- ? function (fn) {
- raf(function () {
- raf(fn);
- });
- } : function (fn) {
- setTimeout(fn, 50);
- };
+// skip type checking this file because we need to attach private properties
+// to elements
- /**
- * A Transition object that encapsulates the state and logic
- * of the transition.
- *
- * @param {Element} el
- * @param {String} id
- * @param {Object} hooks
- * @param {Vue} vm
- */
- function Transition(el, id, hooks, vm) {
- this.id = id;
- this.el = el;
- this.enterClass = hooks && hooks.enterClass || id + '-enter';
- this.leaveClass = hooks && hooks.leaveClass || id + '-leave';
- this.hooks = hooks;
- this.vm = vm;
- // async state
- this.pendingCssEvent = this.pendingCssCb = this.cancel = this.pendingJsCb = this.op = this.cb = null;
- this.justEntered = false;
- this.entered = this.left = false;
- this.typeCache = {};
- // check css transition type
- this.type = hooks && hooks.type;
- /* istanbul ignore if */
- if ('development' !== 'production') {
- if (this.type && this.type !== TYPE_TRANSITION && this.type !== TYPE_ANIMATION) {
- warn('invalid CSS transition type for transition="' + this.id + '": ' + this.type, vm);
- }
- }
- // bind
- var self = this;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone'].forEach(function (m) {
- self[m] = bind(self[m], self);
- });
+function updateDOMListeners (oldVnode, vnode) {
+ if (!oldVnode.data.on && !vnode.data.on) {
+ return
}
-
- var p$1 = Transition.prototype;
-
- /**
- * Start an entering transition.
- *
- * 1. enter transition triggered
- * 2. call beforeEnter hook
- * 3. add enter class
- * 4. insert/show element
- * 5. call enter hook (with possible explicit js callback)
- * 6. reflow
- * 7. based on transition type:
- * - transition:
- * remove class now, wait for transitionend,
- * then done if there's no explicit js callback.
- * - animation:
- * wait for animationend, remove class,
- * then done if there's no explicit js callback.
- * - no css transition:
- * done now if there's no explicit js callback.
- * 8. wait for either done or js callback, then call
- * afterEnter hook.
- *
- * @param {Function} op - insert/show the element
- * @param {Function} [cb]
- */
-
- p$1.enter = function (op, cb) {
- this.cancelPending();
- this.callHook('beforeEnter');
- this.cb = cb;
- addClass(this.el, this.enterClass);
- op();
- this.entered = false;
- this.callHookWithCb('enter');
- if (this.entered) {
- return; // user called done synchronously.
- }
- this.cancel = this.hooks && this.hooks.enterCancelled;
- pushJob(this.enterNextTick);
- };
-
- /**
- * The "nextTick" phase of an entering transition, which is
- * to be pushed into a queue and executed after a reflow so
- * that removing the class can trigger a CSS transition.
- */
-
- p$1.enterNextTick = function () {
- var _this = this;
-
- // prevent transition skipping
- this.justEntered = true;
- waitForTransitionStart(function () {
- _this.justEntered = false;
- });
- var enterDone = this.enterDone;
- var type = this.getCssTransitionType(this.enterClass);
- if (!this.pendingJsCb) {
- if (type === TYPE_TRANSITION) {
- // trigger transition by removing enter class now
- removeClass(this.el, this.enterClass);
- this.setupCssCb(transitionEndEvent, enterDone);
- } else if (type === TYPE_ANIMATION) {
- this.setupCssCb(animationEndEvent, enterDone);
- } else {
- enterDone();
+ var on = vnode.data.on || {};
+ var oldOn = oldVnode.data.on || {};
+ var add = vnode.elm._v_add || (vnode.elm._v_add = function (event, handler, capture) {
+ vnode.elm.addEventListener(event, handler, capture);
+ });
+ var remove = vnode.elm._v_remove || (vnode.elm._v_remove = function (event, handler) {
+ vnode.elm.removeEventListener(event, handler);
+ });
+ updateListeners(on, oldOn, add, remove, vnode.context);
+}
+
+var events = {
+ create: updateDOMListeners,
+ update: updateDOMListeners
+};
+
+/* */
+
+function updateDOMProps (oldVnode, vnode) {
+ if (!oldVnode.data.domProps && !vnode.data.domProps) {
+ return
+ }
+ var key, cur;
+ var elm = vnode.elm;
+ var oldProps = oldVnode.data.domProps || {};
+ var props = vnode.data.domProps || {};
+ // clone observed objects, as the user probably wants to mutate it
+ if (props.__ob__) {
+ props = vnode.data.domProps = extend({}, props);
+ }
+
+ for (key in oldProps) {
+ if (props[key] == null) {
+ elm[key] = undefined;
+ }
+ }
+ for (key in props) {
+ // ignore children if the node has textContent or innerHTML,
+ // as these will throw away existing DOM nodes and cause removal errors
+ // on subsequent patches (#3360)
+ if ((key === 'textContent' || key === 'innerHTML') && vnode.children) {
+ vnode.children.length = 0;
+ }
+ cur = props[key];
+ if (key === 'value') {
+ // store value as _value as well since
+ // non-string values will be stringified
+ elm._value = cur;
+ // avoid resetting cursor position when value is the same
+ var strCur = cur == null ? '' : String(cur);
+ if (elm.value !== strCur && !elm.composing) {
+ elm.value = strCur;
}
- } else if (type === TYPE_TRANSITION) {
- removeClass(this.el, this.enterClass);
+ } else {
+ elm[key] = cur;
}
- };
+ }
+}
- /**
- * The "cleanup" phase of an entering transition.
- */
+var domProps = {
+ create: updateDOMProps,
+ update: updateDOMProps
+};
- p$1.enterDone = function () {
- this.entered = true;
- this.cancel = this.pendingJsCb = null;
- removeClass(this.el, this.enterClass);
- this.callHook('afterEnter');
- if (this.cb) this.cb();
- };
+/* */
- /**
- * Start a leaving transition.
- *
- * 1. leave transition triggered.
- * 2. call beforeLeave hook
- * 3. add leave class (trigger css transition)
- * 4. call leave hook (with possible explicit js callback)
- * 5. reflow if no explicit js callback is provided
- * 6. based on transition type:
- * - transition or animation:
- * wait for end event, remove class, then done if
- * there's no explicit js callback.
- * - no css transition:
- * done if there's no explicit js callback.
- * 7. wait for either done or js callback, then call
- * afterLeave hook.
- *
- * @param {Function} op - remove/hide the element
- * @param {Function} [cb]
- */
+var prefixes = ['Webkit', 'Moz', 'ms'];
- p$1.leave = function (op, cb) {
- this.cancelPending();
- this.callHook('beforeLeave');
- this.op = op;
- this.cb = cb;
- addClass(this.el, this.leaveClass);
- this.left = false;
- this.callHookWithCb('leave');
- if (this.left) {
- return; // user called done synchronously.
- }
- this.cancel = this.hooks && this.hooks.leaveCancelled;
- // only need to handle leaveDone if
- // 1. the transition is already done (synchronously called
- // by the user, which causes this.op set to null)
- // 2. there's no explicit js callback
- if (this.op && !this.pendingJsCb) {
- // if a CSS transition leaves immediately after enter,
- // the transitionend event never fires. therefore we
- // detect such cases and end the leave immediately.
- if (this.justEntered) {
- this.leaveDone();
- } else {
- pushJob(this.leaveNextTick);
- }
+var testEl;
+var normalize = cached(function (prop) {
+ testEl = testEl || document.createElement('div');
+ prop = camelize(prop);
+ if (prop !== 'filter' && (prop in testEl.style)) {
+ return prop
+ }
+ var upper = prop.charAt(0).toUpperCase() + prop.slice(1);
+ for (var i = 0; i < prefixes.length; i++) {
+ var prefixed = prefixes[i] + upper;
+ if (prefixed in testEl.style) {
+ return prefixed
}
- };
+ }
+});
- /**
- * The "nextTick" phase of a leaving transition.
- */
+function updateStyle (oldVnode, vnode) {
+ if ((!oldVnode.data || !oldVnode.data.style) && !vnode.data.style) {
+ return
+ }
+ var cur, name;
+ var el = vnode.elm;
+ var oldStyle = oldVnode.data.style || {};
+ var style = vnode.data.style || {};
- p$1.leaveNextTick = function () {
- var type = this.getCssTransitionType(this.leaveClass);
- if (type) {
- var event = type === TYPE_TRANSITION ? transitionEndEvent : animationEndEvent;
- this.setupCssCb(event, this.leaveDone);
- } else {
- this.leaveDone();
- }
- };
+ // handle string
+ if (typeof style === 'string') {
+ el.style.cssText = style;
+ return
+ }
- /**
- * The "cleanup" phase of a leaving transition.
- */
+ var needClone = style.__ob__;
- p$1.leaveDone = function () {
- this.left = true;
- this.cancel = this.pendingJsCb = null;
- this.op();
- removeClass(this.el, this.leaveClass);
- this.callHook('afterLeave');
- if (this.cb) this.cb();
- this.op = null;
- };
+ // handle array syntax
+ if (Array.isArray(style)) {
+ style = vnode.data.style = toObject(style);
+ }
- /**
- * Cancel any pending callbacks from a previously running
- * but not finished transition.
- */
+ // clone the style for future updates,
+ // in case the user mutates the style object in-place.
+ if (needClone) {
+ style = vnode.data.style = extend({}, style);
+ }
- p$1.cancelPending = function () {
- this.op = this.cb = null;
- var hasPending = false;
- if (this.pendingCssCb) {
- hasPending = true;
- off(this.el, this.pendingCssEvent, this.pendingCssCb);
- this.pendingCssEvent = this.pendingCssCb = null;
- }
- if (this.pendingJsCb) {
- hasPending = true;
- this.pendingJsCb.cancel();
- this.pendingJsCb = null;
+ for (name in oldStyle) {
+ if (style[name] == null) {
+ el.style[normalize(name)] = '';
}
- if (hasPending) {
- removeClass(this.el, this.enterClass);
- removeClass(this.el, this.leaveClass);
- }
- if (this.cancel) {
- this.cancel.call(this.vm, this.el);
- this.cancel = null;
- }
- };
-
- /**
- * Call a user-provided synchronous hook function.
- *
- * @param {String} type
- */
-
- p$1.callHook = function (type) {
- if (this.hooks && this.hooks[type]) {
- this.hooks[type].call(this.vm, this.el);
+ }
+ for (name in style) {
+ cur = style[name];
+ if (cur !== oldStyle[name]) {
+ // ie9 setting to null has no effect, must use empty string
+ el.style[normalize(name)] = cur == null ? '' : cur;
}
- };
-
- /**
- * Call a user-provided, potentially-async hook function.
- * We check for the length of arguments to see if the hook
- * expects a `done` callback. If true, the transition's end
- * will be determined by when the user calls that callback;
- * otherwise, the end is determined by the CSS transition or
- * animation.
- *
- * @param {String} type
- */
+ }
+}
- p$1.callHookWithCb = function (type) {
- var hook = this.hooks && this.hooks[type];
- if (hook) {
- if (hook.length > 1) {
- this.pendingJsCb = cancellable(this[type + 'Done']);
- }
- hook.call(this.vm, this.el, this.pendingJsCb);
- }
- };
+var style = {
+ create: updateStyle,
+ update: updateStyle
+};
- /**
- * Get an element's transition type based on the
- * calculated styles.
- *
- * @param {String} className
- * @return {Number}
- */
+/* */
- p$1.getCssTransitionType = function (className) {
- /* istanbul ignore if */
- if (!transitionEndEvent ||
- // skip CSS transitions if page is not visible -
- // this solves the issue of transitionend events not
- // firing until the page is visible again.
- // pageVisibility API is supported in IE10+, same as
- // CSS transitions.
- document.hidden ||
- // explicit js-only transition
- this.hooks && this.hooks.css === false ||
- // element is hidden
- isHidden(this.el)) {
- return;
- }
- var type = this.type || this.typeCache[className];
- if (type) return type;
- var inlineStyles = this.el.style;
- var computedStyles = window.getComputedStyle(this.el);
- var transDuration = inlineStyles[transDurationProp] || computedStyles[transDurationProp];
- if (transDuration && transDuration !== '0s') {
- type = TYPE_TRANSITION;
+/**
+ * Add class with compatibility for SVG since classList is not supported on
+ * SVG elements in IE
+ */
+function addClass (el, cls) {
+ /* istanbul ignore else */
+ if (el.classList) {
+ if (cls.indexOf(' ') > -1) {
+ cls.split(/\s+/).forEach(function (c) { return el.classList.add(c); });
} else {
- var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp];
- if (animDuration && animDuration !== '0s') {
- type = TYPE_ANIMATION;
- }
+ el.classList.add(cls);
}
- if (type) {
- this.typeCache[className] = type;
+ } else {
+ var cur = ' ' + el.getAttribute('class') + ' ';
+ if (cur.indexOf(' ' + cls + ' ') < 0) {
+ el.setAttribute('class', (cur + cls).trim());
}
- return type;
- };
-
- /**
- * Setup a CSS transitionend/animationend callback.
- *
- * @param {String} event
- * @param {Function} cb
- */
-
- p$1.setupCssCb = function (event, cb) {
- this.pendingCssEvent = event;
- var self = this;
- var el = this.el;
- var onEnd = this.pendingCssCb = function (e) {
- if (e.target === el) {
- off(el, event, onEnd);
- self.pendingCssEvent = self.pendingCssCb = null;
- if (!self.pendingJsCb && cb) {
- cb();
- }
- }
- };
- on(el, event, onEnd);
- };
-
- /**
- * Check if an element is hidden - in that case we can just
- * skip the transition alltogether.
- *
- * @param {Element} el
- * @return {Boolean}
- */
+ }
+}
- function isHidden(el) {
- if (/svg$/.test(el.namespaceURI)) {
- // SVG elements do not have offset(Width|Height)
- // so we need to check the client rect
- var rect = el.getBoundingClientRect();
- return !(rect.width || rect.height);
+/**
+ * Remove class with compatibility for SVG since classList is not supported on
+ * SVG elements in IE
+ */
+function removeClass (el, cls) {
+ /* istanbul ignore else */
+ if (el.classList) {
+ if (cls.indexOf(' ') > -1) {
+ cls.split(/\s+/).forEach(function (c) { return el.classList.remove(c); });
} else {
- return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+ el.classList.remove(cls);
+ }
+ } else {
+ var cur = ' ' + el.getAttribute('class') + ' ';
+ var tar = ' ' + cls + ' ';
+ while (cur.indexOf(tar) >= 0) {
+ cur = cur.replace(tar, ' ');
}
+ el.setAttribute('class', cur.trim());
}
+}
- var transition$1 = {
+/* */
- priority: TRANSITION,
+var hasTransition = inBrowser && !isIE9;
+var TRANSITION = 'transition';
+var ANIMATION = 'animation';
- update: function update(id, oldId) {
- var el = this.el;
- // resolve on owner vm
- var hooks = resolveAsset(this.vm.$options, 'transitions', id);
- id = id || 'v';
- oldId = oldId || 'v';
- el.__v_trans = new Transition(el, id, hooks, this.vm);
- removeClass(el, oldId + '-transition');
- addClass(el, id + '-transition');
- }
+// Transition property/event sniffing
+var transitionProp = 'transition';
+var transitionEndEvent = 'transitionend';
+var animationProp = 'animation';
+var animationEndEvent = 'animationend';
+if (hasTransition) {
+ /* istanbul ignore if */
+ if (window.ontransitionend === undefined &&
+ window.onwebkittransitionend !== undefined) {
+ transitionProp = 'WebkitTransition';
+ transitionEndEvent = 'webkitTransitionEnd';
+ }
+ if (window.onanimationend === undefined &&
+ window.onwebkitanimationend !== undefined) {
+ animationProp = 'WebkitAnimation';
+ animationEndEvent = 'webkitAnimationEnd';
+ }
+}
+
+var raf = (inBrowser && window.requestAnimationFrame) || setTimeout;
+function nextFrame (fn) {
+ raf(function () {
+ raf(fn);
+ });
+}
+
+function addTransitionClass (el, cls) {
+ (el._transitionClasses || (el._transitionClasses = [])).push(cls);
+ addClass(el, cls);
+}
+
+function removeTransitionClass (el, cls) {
+ if (el._transitionClasses) {
+ remove$1(el._transitionClasses, cls);
+ }
+ removeClass(el, cls);
+}
+
+function whenTransitionEnds (
+ el,
+ expectedType,
+ cb
+) {
+ var ref = getTransitionInfo(el, expectedType);
+ var type = ref.type;
+ var timeout = ref.timeout;
+ var propCount = ref.propCount;
+ if (!type) { return cb() }
+ var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
+ var ended = 0;
+ var end = function () {
+ el.removeEventListener(event, onEnd);
+ cb();
};
-
- var internalDirectives = {
- style: style,
- 'class': vClass,
- component: component,
- prop: propDef,
- transition: transition$1
+ var onEnd = function (e) {
+ if (e.target === el) {
+ if (++ended >= propCount) {
+ end();
+ }
+ }
};
-
- // special binding prefixes
- var bindRE = /^v-bind:|^:/;
- var onRE = /^v-on:|^@/;
- var dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/;
- var modifierRE = /\.[^\.]+/g;
- var transitionRE = /^(v-bind:|:)?transition$/;
-
- // default directive priority
- var DEFAULT_PRIORITY = 1000;
- var DEFAULT_TERMINAL_PRIORITY = 2000;
-
- /**
- * Compile a template and return a reusable composite link
- * function, which recursively contains more link functions
- * inside. This top level compile function would normally
- * be called on instance root nodes, but can also be used
- * for partial compilation if the partial argument is true.
- *
- * The returned composite link function, when called, will
- * return an unlink function that tearsdown all directives
- * created during the linking phase.
- *
- * @param {Element|DocumentFragment} el
- * @param {Object} options
- * @param {Boolean} partial
- * @return {Function}
- */
-
- function compile(el, options, partial) {
- // link function for the node itself.
- var nodeLinkFn = partial || !options._asComponent ? compileNode(el, options) : null;
- // link function for the childNodes
- var childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && !isScript(el) && el.hasChildNodes() ? compileNodeList(el.childNodes, options) : null;
-
- /**
- * A composite linker function to be called on a already
- * compiled piece of DOM, which instantiates all directive
- * instances.
- *
- * @param {Vue} vm
- * @param {Element|DocumentFragment} el
- * @param {Vue} [host] - host vm of transcluded content
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - link context fragment
- * @return {Function|undefined}
- */
-
- return function compositeLinkFn(vm, el, host, scope, frag) {
- // cache childNodes before linking parent, fix #657
- var childNodes = toArray(el.childNodes);
- // link
- var dirs = linkAndCapture(function compositeLinkCapturer() {
- if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag);
- if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag);
- }, vm);
- return makeUnlinkFn(vm, dirs);
- };
- }
-
- /**
- * Apply a linker to a vm/element pair and capture the
- * directives created during the process.
- *
- * @param {Function} linker
- * @param {Vue} vm
- */
-
- function linkAndCapture(linker, vm) {
- /* istanbul ignore if */
- if ('development' === 'production') {}
- var originalDirCount = vm._directives.length;
- linker();
- var dirs = vm._directives.slice(originalDirCount);
- dirs.sort(directiveComparator);
- for (var i = 0, l = dirs.length; i < l; i++) {
- dirs[i]._bind();
+ setTimeout(function () {
+ if (ended < propCount) {
+ end();
+ }
+ }, timeout + 1);
+ el.addEventListener(event, onEnd);
+}
+
+var transformRE = /\b(transform|all)(,|$)/;
+
+function getTransitionInfo (el, expectedType) {
+ var styles = window.getComputedStyle(el);
+ var transitioneDelays = styles[transitionProp + 'Delay'].split(', ');
+ var transitionDurations = styles[transitionProp + 'Duration'].split(', ');
+ var transitionTimeout = getTimeout(transitioneDelays, transitionDurations);
+ var animationDelays = styles[animationProp + 'Delay'].split(', ');
+ var animationDurations = styles[animationProp + 'Duration'].split(', ');
+ var animationTimeout = getTimeout(animationDelays, animationDurations);
+
+ var type;
+ var timeout = 0;
+ var propCount = 0;
+ /* istanbul ignore if */
+ if (expectedType === TRANSITION) {
+ if (transitionTimeout > 0) {
+ type = TRANSITION;
+ timeout = transitionTimeout;
+ propCount = transitionDurations.length;
+ }
+ } else if (expectedType === ANIMATION) {
+ if (animationTimeout > 0) {
+ type = ANIMATION;
+ timeout = animationTimeout;
+ propCount = animationDurations.length;
}
- return dirs;
- }
-
- /**
- * Directive priority sort comparator
- *
- * @param {Object} a
- * @param {Object} b
- */
-
- function directiveComparator(a, b) {
- a = a.descriptor.def.priority || DEFAULT_PRIORITY;
- b = b.descriptor.def.priority || DEFAULT_PRIORITY;
- return a > b ? -1 : a === b ? 0 : 1;
+ } else {
+ timeout = Math.max(transitionTimeout, animationTimeout);
+ type = timeout > 0
+ ? transitionTimeout > animationTimeout
+ ? TRANSITION
+ : ANIMATION
+ : null;
+ propCount = type
+ ? type === TRANSITION
+ ? transitionDurations.length
+ : animationDurations.length
+ : 0;
+ }
+ var hasTransform =
+ type === TRANSITION &&
+ transformRE.test(styles[transitionProp + 'Property']);
+ return {
+ type: type,
+ timeout: timeout,
+ propCount: propCount,
+ hasTransform: hasTransform
+ }
+}
+
+function getTimeout (delays, durations) {
+ return Math.max.apply(null, durations.map(function (d, i) {
+ return toMs(d) + toMs(delays[i])
+ }))
+}
+
+function toMs (s) {
+ return Number(s.slice(0, -1)) * 1000
+}
+
+/* */
+
+function enter (vnode) {
+ var el = vnode.elm;
+
+ // call leave callback now
+ if (el._leaveCb) {
+ el._leaveCb.cancelled = true;
+ el._leaveCb();
+ }
+
+ var data = resolveTransition(vnode.data.transition);
+ if (!data) {
+ return
}
- /**
- * Linker functions return an unlink function that
- * tearsdown all directives instances generated during
- * the process.
- *
- * We create unlink functions with only the necessary
- * information to avoid retaining additional closures.
- *
- * @param {Vue} vm
- * @param {Array} dirs
- * @param {Vue} [context]
- * @param {Array} [contextDirs]
- * @return {Function}
- */
+ /* istanbul ignore if */
+ if (el._enterCb || el.nodeType !== 1) {
+ return
+ }
+
+ var css = data.css;
+ var type = data.type;
+ var enterClass = data.enterClass;
+ var enterActiveClass = data.enterActiveClass;
+ var appearClass = data.appearClass;
+ var appearActiveClass = data.appearActiveClass;
+ var beforeEnter = data.beforeEnter;
+ var enter = data.enter;
+ var afterEnter = data.afterEnter;
+ var enterCancelled = data.enterCancelled;
+ var beforeAppear = data.beforeAppear;
+ var appear = data.appear;
+ var afterAppear = data.afterAppear;
+ var appearCancelled = data.appearCancelled;
+
+ // activeInstance will always be the <transition> component managing this
+ // transition. One edge case to check is when the <transition> is placed
+ // as the root node of a child component. In that case we need to check
+ // <transition>'s parent for appear check.
+ var transitionNode = activeInstance.$vnode;
+ var context = transitionNode && transitionNode.parent
+ ? transitionNode.parent.context
+ : activeInstance;
+
+ var isAppear = !context._isMounted || !vnode.isRootInsert;
+
+ if (isAppear && !appear && appear !== '') {
+ return
+ }
+
+ var startClass = isAppear ? appearClass : enterClass;
+ var activeClass = isAppear ? appearActiveClass : enterActiveClass;
+ var beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter;
+ var enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter;
+ var afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter;
+ var enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled;
+
+ var expectsCSS = css !== false && !isIE9;
+ var userWantsControl =
+ enterHook &&
+ // enterHook may be a bound method which exposes
+ // the length of original fn as _length
+ (enterHook._length || enterHook.length) > 1;
+
+ var cb = el._enterCb = once(function () {
+ if (expectsCSS) {
+ removeTransitionClass(el, activeClass);
+ }
+ if (cb.cancelled) {
+ if (expectsCSS) {
+ removeTransitionClass(el, startClass);
+ }
+ enterCancelledHook && enterCancelledHook(el);
+ } else {
+ afterEnterHook && afterEnterHook(el);
+ }
+ el._enterCb = null;
+ });
- function makeUnlinkFn(vm, dirs, context, contextDirs) {
- function unlink(destroying) {
- teardownDirs(vm, dirs, destroying);
- if (context && contextDirs) {
- teardownDirs(context, contextDirs);
+ if (!vnode.data.show) {
+ // remove pending leave element on enter by injecting an insert hook
+ mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', function () {
+ var parent = el.parentNode;
+ var pendingNode = parent && parent._pending && parent._pending[vnode.key];
+ if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) {
+ pendingNode.elm._leaveCb();
}
- }
- // expose linked directives
- unlink.dirs = dirs;
- return unlink;
+ enterHook && enterHook(el, cb);
+ }, 'transition-insert');
}
- /**
- * Teardown partial linked directives.
- *
- * @param {Vue} vm
- * @param {Array} dirs
- * @param {Boolean} destroying
- */
-
- function teardownDirs(vm, dirs, destroying) {
- var i = dirs.length;
- while (i--) {
- dirs[i]._teardown();
- if ('development' !== 'production' && !destroying) {
- vm._directives.$remove(dirs[i]);
+ // start enter transition
+ beforeEnterHook && beforeEnterHook(el);
+ if (expectsCSS) {
+ addTransitionClass(el, startClass);
+ addTransitionClass(el, activeClass);
+ nextFrame(function () {
+ removeTransitionClass(el, startClass);
+ if (!cb.cancelled && !userWantsControl) {
+ whenTransitionEnds(el, type, cb);
}
- }
+ });
}
- /**
- * Compile link props on an instance.
- *
- * @param {Vue} vm
- * @param {Element} el
- * @param {Object} props
- * @param {Object} [scope]
- * @return {Function}
- */
-
- function compileAndLinkProps(vm, el, props, scope) {
- var propsLinkFn = compileProps(el, props, vm);
- var propDirs = linkAndCapture(function () {
- propsLinkFn(vm, scope);
- }, vm);
- return makeUnlinkFn(vm, propDirs);
+ if (vnode.data.show) {
+ enterHook && enterHook(el, cb);
}
- /**
- * Compile the root element of an instance.
- *
- * 1. attrs on context container (context scope)
- * 2. attrs on the component template root node, if
- * replace:true (child scope)
- *
- * If this is a fragment instance, we only need to compile 1.
- *
- * @param {Element} el
- * @param {Object} options
- * @param {Object} contextOptions
- * @return {Function}
- */
-
- function compileRoot(el, options, contextOptions) {
- var containerAttrs = options._containerAttrs;
- var replacerAttrs = options._replacerAttrs;
- var contextLinkFn, replacerLinkFn;
-
- // only need to compile other attributes for
- // non-fragment instances
- if (el.nodeType !== 11) {
- // for components, container and replacer need to be
- // compiled separately and linked in different scopes.
- if (options._asComponent) {
- // 2. container attributes
- if (containerAttrs && contextOptions) {
- contextLinkFn = compileDirectives(containerAttrs, contextOptions);
- }
- if (replacerAttrs) {
- // 3. replacer attributes
- replacerLinkFn = compileDirectives(replacerAttrs, options);
- }
- } else {
- // non-component, just compile as a normal element.
- replacerLinkFn = compileDirectives(el.attributes, options);
- }
- } else if ('development' !== 'production' && containerAttrs) {
- // warn container directives for fragment instances
- var names = containerAttrs.filter(function (attr) {
- // allow vue-loader/vueify scoped css attributes
- return attr.name.indexOf('_v-') < 0 &&
- // allow event listeners
- !onRE.test(attr.name) &&
- // allow slots
- attr.name !== 'slot';
- }).map(function (attr) {
- return '"' + attr.name + '"';
- });
- if (names.length) {
- var plural = names.length > 1;
- warn('Attribute' + (plural ? 's ' : ' ') + names.join(', ') + (plural ? ' are' : ' is') + ' ignored on component ' + '<' + options.el.tagName.toLowerCase() + '> because ' + 'the component is a fragment instance: ' + 'http://vuejs.org/guide/components.html#Fragment-Instance');
- }
- }
-
- options._containerAttrs = options._replacerAttrs = null;
- return function rootLinkFn(vm, el, scope) {
- // link context scope dirs
- var context = vm._context;
- var contextDirs;
- if (context && contextLinkFn) {
- contextDirs = linkAndCapture(function () {
- contextLinkFn(context, el, null, scope);
- }, context);
- }
+ if (!expectsCSS && !userWantsControl) {
+ cb();
+ }
+}
- // link self
- var selfDirs = linkAndCapture(function () {
- if (replacerLinkFn) replacerLinkFn(vm, el);
- }, vm);
+function leave (vnode, rm) {
+ var el = vnode.elm;
- // return the unlink function that tearsdown context
- // container directives.
- return makeUnlinkFn(vm, selfDirs, context, contextDirs);
- };
+ // call enter callback now
+ if (el._enterCb) {
+ el._enterCb.cancelled = true;
+ el._enterCb();
}
- /**
- * Compile a node and return a nodeLinkFn based on the
- * node type.
- *
- * @param {Node} node
- * @param {Object} options
- * @return {Function|null}
- */
+ var data = resolveTransition(vnode.data.transition);
+ if (!data) {
+ return rm()
+ }
- function compileNode(node, options) {
- var type = node.nodeType;
- if (type === 1 && !isScript(node)) {
- return compileElement(node, options);
- } else if (type === 3 && node.data.trim()) {
- return compileTextNode(node, options);
+ /* istanbul ignore if */
+ if (el._leaveCb || el.nodeType !== 1) {
+ return
+ }
+
+ var css = data.css;
+ var type = data.type;
+ var leaveClass = data.leaveClass;
+ var leaveActiveClass = data.leaveActiveClass;
+ var beforeLeave = data.beforeLeave;
+ var leave = data.leave;
+ var afterLeave = data.afterLeave;
+ var leaveCancelled = data.leaveCancelled;
+ var delayLeave = data.delayLeave;
+
+ var expectsCSS = css !== false && !isIE9;
+ var userWantsControl =
+ leave &&
+ // leave hook may be a bound method which exposes
+ // the length of original fn as _length
+ (leave._length || leave.length) > 1;
+
+ var cb = el._leaveCb = once(function () {
+ if (el.parentNode && el.parentNode._pending) {
+ el.parentNode._pending[vnode.key] = null;
+ }
+ if (expectsCSS) {
+ removeTransitionClass(el, leaveActiveClass);
+ }
+ if (cb.cancelled) {
+ if (expectsCSS) {
+ removeTransitionClass(el, leaveClass);
+ }
+ leaveCancelled && leaveCancelled(el);
} else {
- return null;
+ rm();
+ afterLeave && afterLeave(el);
}
- }
+ el._leaveCb = null;
+ });
- /**
- * Compile an element and return a nodeLinkFn.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|null}
- */
+ if (delayLeave) {
+ delayLeave(performLeave);
+ } else {
+ performLeave();
+ }
- function compileElement(el, options) {
- // preprocess textareas.
- // textarea treats its text content as the initial value.
- // just bind it as an attr directive for value.
- if (el.tagName === 'TEXTAREA') {
- var tokens = parseText(el.value);
- if (tokens) {
- el.setAttribute(':value', tokensToExp(tokens));
- el.value = '';
- }
- }
- var linkFn;
- var hasAttrs = el.hasAttributes();
- var attrs = hasAttrs && toArray(el.attributes);
- // check terminal directives (for & if)
- if (hasAttrs) {
- linkFn = checkTerminalDirectives(el, attrs, options);
+ function performLeave () {
+ // the delayed leave may have already been cancelled
+ if (cb.cancelled) {
+ return
}
- // check element directives
- if (!linkFn) {
- linkFn = checkElementDirectives(el, options);
+ // record leaving element
+ if (!vnode.data.show) {
+ (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode;
}
- // check component
- if (!linkFn) {
- linkFn = checkComponent(el, options);
+ beforeLeave && beforeLeave(el);
+ if (expectsCSS) {
+ addTransitionClass(el, leaveClass);
+ addTransitionClass(el, leaveActiveClass);
+ nextFrame(function () {
+ removeTransitionClass(el, leaveClass);
+ if (!cb.cancelled && !userWantsControl) {
+ whenTransitionEnds(el, type, cb);
+ }
+ });
}
- // normal directives
- if (!linkFn && hasAttrs) {
- linkFn = compileDirectives(attrs, options);
+ leave && leave(el, cb);
+ if (!expectsCSS && !userWantsControl) {
+ cb();
}
- return linkFn;
}
+}
- /**
- * Compile a textNode and return a nodeLinkFn.
- *
- * @param {TextNode} node
- * @param {Object} options
- * @return {Function|null} textNodeLinkFn
- */
-
- function compileTextNode(node, options) {
- // skip marked text nodes
- if (node._skip) {
- return removeText;
- }
-
- var tokens = parseText(node.wholeText);
- if (!tokens) {
- return null;
- }
-
- // mark adjacent text nodes as skipped,
- // because we are using node.wholeText to compile
- // all adjacent text nodes together. This fixes
- // issues in IE where sometimes it splits up a single
- // text node into multiple ones.
- var next = node.nextSibling;
- while (next && next.nodeType === 3) {
- next._skip = true;
- next = next.nextSibling;
- }
-
- var frag = document.createDocumentFragment();
- var el, token;
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i];
- el = token.tag ? processTextToken(token, options) : document.createTextNode(token.value);
- frag.appendChild(el);
+function resolveTransition (def$$1) {
+ if (!def$$1) {
+ return
+ }
+ /* istanbul ignore else */
+ if (typeof def$$1 === 'object') {
+ var res = {};
+ if (def$$1.css !== false) {
+ extend(res, autoCssTransition(def$$1.name || 'v'));
}
- return makeTextNodeLinkFn(tokens, frag, options);
+ extend(res, def$$1);
+ return res
+ } else if (typeof def$$1 === 'string') {
+ return autoCssTransition(def$$1)
}
+}
- /**
- * Linker for an skipped text node.
- *
- * @param {Vue} vm
- * @param {Text} node
- */
-
- function removeText(vm, node) {
- remove(node);
+var autoCssTransition = cached(function (name) {
+ return {
+ enterClass: (name + "-enter"),
+ leaveClass: (name + "-leave"),
+ appearClass: (name + "-enter"),
+ enterActiveClass: (name + "-enter-active"),
+ leaveActiveClass: (name + "-leave-active"),
+ appearActiveClass: (name + "-enter-active")
}
+});
- /**
- * Process a single text token.
- *
- * @param {Object} token
- * @param {Object} options
- * @return {Node}
- */
+function once (fn) {
+ var called = false;
+ return function () {
+ if (!called) {
+ called = true;
+ fn();
+ }
+ }
+}
- function processTextToken(token, options) {
- var el;
- if (token.oneTime) {
- el = document.createTextNode(token.value);
+var transition = inBrowser ? {
+ create: function create (_, vnode) {
+ if (!vnode.data.show) {
+ enter(vnode);
+ }
+ },
+ remove: function remove (vnode, rm) {
+ /* istanbul ignore else */
+ if (!vnode.data.show) {
+ leave(vnode, rm);
} else {
- if (token.html) {
- el = document.createComment('v-html');
- setTokenType('html');
- } else {
- // IE will clean up empty textNodes during
- // frag.cloneNode(true), so we have to give it
- // something here...
- el = document.createTextNode(' ');
- setTokenType('text');
- }
- }
- function setTokenType(type) {
- if (token.descriptor) return;
- var parsed = parseDirective(token.value);
- token.descriptor = {
- name: type,
- def: directives[type],
- expression: parsed.expression,
- filters: parsed.filters
- };
+ rm();
}
- return el;
}
+} : {};
- /**
- * Build a function that processes a textNode.
- *
- * @param {Array<Object>} tokens
- * @param {DocumentFragment} frag
- */
-
- function makeTextNodeLinkFn(tokens, frag) {
- return function textNodeLinkFn(vm, el, host, scope) {
- var fragClone = frag.cloneNode(true);
- var childNodes = toArray(fragClone.childNodes);
- var token, value, node;
- for (var i = 0, l = tokens.length; i < l; i++) {
- token = tokens[i];
- value = token.value;
- if (token.tag) {
- node = childNodes[i];
- if (token.oneTime) {
- value = (scope || vm).$eval(value);
- if (token.html) {
- replace(node, parseTemplate(value, true));
- } else {
- node.data = _toString(value);
- }
- } else {
- vm._bindDir(token.descriptor, node, host, scope);
- }
- }
- }
- replace(el, fragClone);
- };
- }
+var platformModules = [
+ attrs,
+ klass,
+ events,
+ domProps,
+ style,
+ transition
+];
- /**
- * Compile a node list and return a childLinkFn.
- *
- * @param {NodeList} nodeList
- * @param {Object} options
- * @return {Function|undefined}
- */
+/* */
- function compileNodeList(nodeList, options) {
- var linkFns = [];
- var nodeLinkFn, childLinkFn, node;
- for (var i = 0, l = nodeList.length; i < l; i++) {
- node = nodeList[i];
- nodeLinkFn = compileNode(node, options);
- childLinkFn = !(nodeLinkFn && nodeLinkFn.terminal) && node.tagName !== 'SCRIPT' && node.hasChildNodes() ? compileNodeList(node.childNodes, options) : null;
- linkFns.push(nodeLinkFn, childLinkFn);
- }
- return linkFns.length ? makeChildLinkFn(linkFns) : null;
- }
+// the directive module should be applied last, after all
+// built-in modules have been applied.
+var modules = platformModules.concat(baseModules);
- /**
- * Make a child link function for a node's childNodes.
- *
- * @param {Array<Function>} linkFns
- * @return {Function} childLinkFn
- */
+var patch$1 = createPatchFunction({ nodeOps: nodeOps, modules: modules });
- function makeChildLinkFn(linkFns) {
- return function childLinkFn(vm, nodes, host, scope, frag) {
- var node, nodeLinkFn, childrenLinkFn;
- for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
- node = nodes[n];
- nodeLinkFn = linkFns[i++];
- childrenLinkFn = linkFns[i++];
- // cache childNodes before linking parent, fix #657
- var childNodes = toArray(node.childNodes);
- if (nodeLinkFn) {
- nodeLinkFn(vm, node, host, scope, frag);
- }
- if (childrenLinkFn) {
- childrenLinkFn(vm, childNodes, host, scope, frag);
- }
- }
- };
- }
+/**
+ * Not type checking this file because flow doesn't like attaching
+ * properties to Elements.
+ */
- /**
- * Check for element directives (custom elements that should
- * be resovled as terminal directives).
- *
- * @param {Element} el
- * @param {Object} options
- */
+var modelableTagRE = /^input|select|textarea|vue-component-[0-9]+(-[0-9a-zA-Z_\-]*)?$/;
- function checkElementDirectives(el, options) {
- var tag = el.tagName.toLowerCase();
- if (commonTagRE.test(tag)) {
- return;
+/* istanbul ignore if */
+if (isIE9) {
+ // http://www.matts411.com/post/internet-explorer-9-oninput/
+ document.addEventListener('selectionchange', function () {
+ var el = document.activeElement;
+ if (el && el.vmodel) {
+ trigger(el, 'input');
}
- var def = resolveAsset(options, 'elementDirectives', tag);
- if (def) {
- return makeTerminalNodeLinkFn(el, tag, '', options, def);
+ });
+}
+
+var model = {
+ inserted: function inserted (el, binding, vnode) {
+ {
+ if (!modelableTagRE.test(vnode.tag)) {
+ warn(
+ "v-model is not supported on element type: <" + (vnode.tag) + ">. " +
+ 'If you are working with contenteditable, it\'s recommended to ' +
+ 'wrap a library dedicated for that purpose inside a custom component.',
+ vnode.context
+ );
+ }
}
- }
-
- /**
- * Check if an element is a component. If yes, return
- * a component link function.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Function|undefined}
- */
-
- function checkComponent(el, options) {
- var component = checkComponentAttr(el, options);
- if (component) {
- var ref = findRef(el);
- var descriptor = {
- name: 'component',
- ref: ref,
- expression: component.id,
- def: internalDirectives.component,
- modifiers: {
- literal: !component.dynamic
- }
- };
- var componentLinkFn = function componentLinkFn(vm, el, host, scope, frag) {
- if (ref) {
- defineReactive((scope || vm).$refs, ref, null);
- }
- vm._bindDir(descriptor, el, host, scope, frag);
+ if (vnode.tag === 'select') {
+ var cb = function () {
+ setSelected(el, binding, vnode.context);
};
- componentLinkFn.terminal = true;
- return componentLinkFn;
- }
- }
-
- /**
- * Check an element for terminal directives in fixed order.
- * If it finds one, return a terminal link function.
- *
- * @param {Element} el
- * @param {Array} attrs
- * @param {Object} options
- * @return {Function} terminalLinkFn
- */
-
- function checkTerminalDirectives(el, attrs, options) {
- // skip v-pre
- if (getAttr(el, 'v-pre') !== null) {
- return skip;
- }
- // skip v-else block, but only if following v-if
- if (el.hasAttribute('v-else')) {
- var prev = el.previousElementSibling;
- if (prev && prev.hasAttribute('v-if')) {
- return skip;
- }
- }
-
- var attr, name, value, modifiers, matched, dirName, rawName, arg, def, termDef;
- for (var i = 0, j = attrs.length; i < j; i++) {
- attr = attrs[i];
- name = attr.name.replace(modifierRE, '');
- if (matched = name.match(dirAttrRE)) {
- def = resolveAsset(options, 'directives', matched[1]);
- if (def && def.terminal) {
- if (!termDef || (def.priority || DEFAULT_TERMINAL_PRIORITY) > termDef.priority) {
- termDef = def;
- rawName = attr.name;
- modifiers = parseModifiers(attr.name);
- value = attr.value;
- dirName = matched[1];
- arg = matched[2];
- }
+ cb();
+ /* istanbul ignore if */
+ if (isIE || isEdge) {
+ setTimeout(cb, 0);
+ }
+ } else if (
+ (vnode.tag === 'textarea' || el.type === 'text') &&
+ !binding.modifiers.lazy
+ ) {
+ if (!isAndroid) {
+ el.addEventListener('compositionstart', onCompositionStart);
+ el.addEventListener('compositionend', onCompositionEnd);
+ }
+ /* istanbul ignore if */
+ if (isIE9) {
+ el.vmodel = true;
+ }
+ }
+ },
+ componentUpdated: function componentUpdated (el, binding, vnode) {
+ if (vnode.tag === 'select') {
+ setSelected(el, binding, vnode.context);
+ // in case the options rendered by v-for have changed,
+ // it's possible that the value is out-of-sync with the rendered options.
+ // detect such cases and filter out values that no longer has a matchig
+ // option in the DOM.
+ var needReset = el.multiple
+ ? binding.value.some(function (v) { return hasNoMatchingOption(v, el.options); })
+ : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, el.options);
+ if (needReset) {
+ trigger(el, 'change');
+ }
+ }
+ }
+};
+
+function setSelected (el, binding, vm) {
+ var value = binding.value;
+ var isMultiple = el.multiple;
+ if (isMultiple && !Array.isArray(value)) {
+ "development" !== 'production' && warn(
+ "<select multiple v-model=\"" + (binding.expression) + "\"> " +
+ "expects an Array value for its binding, but got " + (Object.prototype.toString.call(value).slice(8, -1)),
+ vm
+ );
+ return
+ }
+ var selected, option;
+ for (var i = 0, l = el.options.length; i < l; i++) {
+ option = el.options[i];
+ if (isMultiple) {
+ selected = looseIndexOf(value, getValue(option)) > -1;
+ if (option.selected !== selected) {
+ option.selected = selected;
+ }
+ } else {
+ if (looseEqual(getValue(option), value)) {
+ if (el.selectedIndex !== i) {
+ el.selectedIndex = i;
}
+ return
}
}
+ }
+ if (!isMultiple) {
+ el.selectedIndex = -1;
+ }
+}
- if (termDef) {
- return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
+function hasNoMatchingOption (value, options) {
+ for (var i = 0, l = options.length; i < l; i++) {
+ if (looseEqual(getValue(options[i]), value)) {
+ return false
}
}
+ return true
+}
- function skip() {}
- skip.terminal = true;
+function getValue (option) {
+ return '_value' in option
+ ? option._value
+ : option.value
+}
- /**
- * Build a node link function for a terminal directive.
- * A terminal link function terminates the current
- * compilation recursion and handles compilation of the
- * subtree in the directive.
- *
- * @param {Element} el
- * @param {String} dirName
- * @param {String} value
- * @param {Object} options
- * @param {Object} def
- * @param {String} [rawName]
- * @param {String} [arg]
- * @param {Object} [modifiers]
- * @return {Function} terminalLinkFn
- */
+function onCompositionStart (e) {
+ e.target.composing = true;
+}
- function makeTerminalNodeLinkFn(el, dirName, value, options, def, rawName, arg, modifiers) {
- var parsed = parseDirective(value);
- var descriptor = {
- name: dirName,
- arg: arg,
- expression: parsed.expression,
- filters: parsed.filters,
- raw: value,
- attr: rawName,
- modifiers: modifiers,
- def: def
- };
- // check ref for v-for and router-view
- if (dirName === 'for' || dirName === 'router-view') {
- descriptor.ref = findRef(el);
- }
- var fn = function terminalNodeLinkFn(vm, el, host, scope, frag) {
- if (descriptor.ref) {
- defineReactive((scope || vm).$refs, descriptor.ref, null);
- }
- vm._bindDir(descriptor, el, host, scope, frag);
- };
- fn.terminal = true;
- return fn;
- }
+function onCompositionEnd (e) {
+ e.target.composing = false;
+ trigger(e.target, 'input');
+}
- /**
- * Compile the directives on an element and return a linker.
- *
- * @param {Array|NamedNodeMap} attrs
- * @param {Object} options
- * @return {Function}
- */
+function trigger (el, type) {
+ var e = document.createEvent('HTMLEvents');
+ e.initEvent(type, true, true);
+ el.dispatchEvent(e);
+}
- function compileDirectives(attrs, options) {
- var i = attrs.length;
- var dirs = [];
- var attr, name, value, rawName, rawValue, dirName, arg, modifiers, dirDef, tokens, matched;
- while (i--) {
- attr = attrs[i];
- name = rawName = attr.name;
- value = rawValue = attr.value;
- tokens = parseText(value);
- // reset arg
- arg = null;
- // check modifiers
- modifiers = parseModifiers(name);
- name = name.replace(modifierRE, '');
-
- // attribute interpolations
- if (tokens) {
- value = tokensToExp(tokens);
- arg = name;
- pushDir('bind', directives.bind, tokens);
- // warn against mixing mustaches with v-bind
- if ('development' !== 'production') {
- if (name === 'class' && Array.prototype.some.call(attrs, function (attr) {
- return attr.name === ':class' || attr.name === 'v-bind:class';
- })) {
- warn('class="' + rawValue + '": Do not mix mustache interpolation ' + 'and v-bind for "class" on the same element. Use one or the other.', options);
- }
- }
- } else
-
- // special attribute: transition
- if (transitionRE.test(name)) {
- modifiers.literal = !bindRE.test(name);
- pushDir('transition', internalDirectives.transition);
- } else
-
- // event handlers
- if (onRE.test(name)) {
- arg = name.replace(onRE, '');
- pushDir('on', directives.on);
- } else
-
- // attribute bindings
- if (bindRE.test(name)) {
- dirName = name.replace(bindRE, '');
- if (dirName === 'style' || dirName === 'class') {
- pushDir(dirName, internalDirectives[dirName]);
- } else {
- arg = dirName;
- pushDir('bind', directives.bind);
- }
- } else
-
- // normal directives
- if (matched = name.match(dirAttrRE)) {
- dirName = matched[1];
- arg = matched[2];
-
- // skip v-else (when used with v-show)
- if (dirName === 'else') {
- continue;
- }
-
- dirDef = resolveAsset(options, 'directives', dirName, true);
- if (dirDef) {
- pushDir(dirName, dirDef);
- }
- }
- }
+/* */
- /**
- * Push a directive.
- *
- * @param {String} dirName
- * @param {Object|Function} def
- * @param {Array} [interpTokens]
- */
-
- function pushDir(dirName, def, interpTokens) {
- var hasOneTimeToken = interpTokens && hasOneTime(interpTokens);
- var parsed = !hasOneTimeToken && parseDirective(value);
- dirs.push({
- name: dirName,
- attr: rawName,
- raw: rawValue,
- def: def,
- arg: arg,
- modifiers: modifiers,
- // conversion from interpolation strings with one-time token
- // to expression is differed until directive bind time so that we
- // have access to the actual vm context for one-time bindings.
- expression: parsed && parsed.expression,
- filters: parsed && parsed.filters,
- interp: interpTokens,
- hasOneTime: hasOneTimeToken
- });
- }
+// recursively search for possible transition defined inside the component root
+function locateNode (vnode) {
+ return vnode.child && (!vnode.data || !vnode.data.transition)
+ ? locateNode(vnode.child._vnode)
+ : vnode
+}
- if (dirs.length) {
- return makeNodeLinkFn(dirs);
- }
- }
+var show = {
+ bind: function bind (el, ref, vnode) {
+ var value = ref.value;
- /**
- * Parse modifiers from directive attribute name.
- *
- * @param {String} name
- * @return {Object}
- */
+ vnode = locateNode(vnode);
+ var transition = vnode.data && vnode.data.transition;
+ if (value && transition && !isIE9) {
+ enter(vnode);
+ }
+ var originalDisplay = el.style.display === 'none' ? '' : el.style.display;
+ el.style.display = value ? originalDisplay : 'none';
+ el.__vOriginalDisplay = originalDisplay;
+ },
+ update: function update (el, ref, vnode) {
+ var value = ref.value;
+ var oldValue = ref.oldValue;
- function parseModifiers(name) {
- var res = Object.create(null);
- var match = name.match(modifierRE);
- if (match) {
- var i = match.length;
- while (i--) {
- res[match[i].slice(1)] = true;
+ /* istanbul ignore if */
+ if (value === oldValue) { return }
+ vnode = locateNode(vnode);
+ var transition = vnode.data && vnode.data.transition;
+ if (transition && !isIE9) {
+ if (value) {
+ enter(vnode);
+ el.style.display = el.__vOriginalDisplay;
+ } else {
+ leave(vnode, function () {
+ el.style.display = 'none';
+ });
}
- }
- return res;
+ } else {
+ el.style.display = value ? el.__vOriginalDisplay : 'none';
+ }
+ }
+};
+
+var platformDirectives = {
+ model: model,
+ show: show
+};
+
+/* */
+
+// Provides transition support for a single element/component.
+// supports transition mode (out-in / in-out)
+
+var transitionProps = {
+ name: String,
+ appear: Boolean,
+ css: Boolean,
+ mode: String,
+ type: String,
+ enterClass: String,
+ leaveClass: String,
+ enterActiveClass: String,
+ leaveActiveClass: String,
+ appearClass: String,
+ appearActiveClass: String
+};
+
+// in case the child is also an abstract component, e.g. <keep-alive>
+// we want to recrusively retrieve the real component to be rendered
+function getRealChild (vnode) {
+ var compOptions = vnode && vnode.componentOptions;
+ if (compOptions && compOptions.Ctor.options.abstract) {
+ return getRealChild(getFirstComponentChild(compOptions.children))
+ } else {
+ return vnode
}
+}
- /**
- * Build a link function for all directives on a single node.
- *
- * @param {Array} directives
- * @return {Function} directivesLinkFn
- */
-
- function makeNodeLinkFn(directives) {
- return function nodeLinkFn(vm, el, host, scope, frag) {
- // reverse apply because it's sorted low to high
- var i = directives.length;
- while (i--) {
- vm._bindDir(directives[i], el, host, scope, frag);
- }
- };
+function extractTransitionData (comp) {
+ var data = {};
+ var options = comp.$options;
+ // props
+ for (var key in options.propsData) {
+ data[key] = comp[key];
+ }
+ // events.
+ // extract listeners and pass them directly to the transition methods
+ var listeners = options._parentListeners;
+ for (var key$1 in listeners) {
+ data[camelize(key$1)] = listeners[key$1].fn;
}
+ return data
+}
- /**
- * Check if an interpolation string contains one-time tokens.
- *
- * @param {Array} tokens
- * @return {Boolean}
- */
+function placeholder (h, rawChild) {
+ return /\d-keep-alive$/.test(rawChild.tag)
+ ? h('keep-alive')
+ : null
+}
- function hasOneTime(tokens) {
- var i = tokens.length;
- while (i--) {
- if (tokens[i].oneTime) return true;
+function hasParentTransition (vnode) {
+ while ((vnode = vnode.parent)) {
+ if (vnode.data.transition) {
+ return true
}
}
+}
- function isScript(el) {
- return el.tagName === 'SCRIPT' && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript');
- }
-
- var specialCharRE = /[^\w\-:\.]/;
+var Transition = {
+ name: 'transition',
+ props: transitionProps,
+ abstract: true,
+ render: function render (h) {
+ var this$1 = this;
- /**
- * Process an element or a DocumentFragment based on a
- * instance option object. This allows us to transclude
- * a template node/fragment before the instance is created,
- * so the processed fragment can then be cloned and reused
- * in v-for.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Element|DocumentFragment}
- */
-
- function transclude(el, options) {
- // extract container attributes to pass them down
- // to compiler, because they need to be compiled in
- // parent scope. we are mutating the options object here
- // assuming the same object will be used for compile
- // right after this.
- if (options) {
- options._containerAttrs = extractAttrs(el);
+ var children = this.$slots.default;
+ if (!children) {
+ return
}
- // for template tags, what we want is its content as
- // a documentFragment (for fragment instances)
- if (isTemplate(el)) {
- el = parseTemplate(el);
- }
- if (options) {
- if (options._asComponent && !options.template) {
- options.template = '<slot></slot>';
- }
- if (options.template) {
- options._content = extractContent(el);
- el = transcludeTemplate(el, options);
- }
+
+ // filter out text nodes (possible whitespaces)
+ children = children.filter(function (c) { return c.tag; });
+ /* istanbul ignore if */
+ if (!children.length) {
+ return
}
- if (isFragment(el)) {
- // anchors for fragment instance
- // passing in `persist: true` to avoid them being
- // discarded by IE during template cloning
- prepend(createAnchor('v-start', true), el);
- el.appendChild(createAnchor('v-end', true));
+
+ // warn multiple elements
+ if ("development" !== 'production' && children.length > 1) {
+ warn(
+ '<transition> can only be used on a single element. Use ' +
+ '<transition-group> for lists.',
+ this.$parent
+ );
}
- return el;
- }
- /**
- * Process the template option.
- * If the replace option is true this will swap the $el.
- *
- * @param {Element} el
- * @param {Object} options
- * @return {Element|DocumentFragment}
- */
+ var mode = this.mode;
- function transcludeTemplate(el, options) {
- var template = options.template;
- var frag = parseTemplate(template, true);
- if (frag) {
- var replacer = frag.firstChild;
- var tag = replacer.tagName && replacer.tagName.toLowerCase();
- if (options.replace) {
- /* istanbul ignore if */
- if (el === document.body) {
- 'development' !== 'production' && warn('You are mounting an instance with a template to ' + '<body>. This will replace <body> entirely. You ' + 'should probably use `replace: false` here.');
- }
- // there are many cases where the instance must
- // become a fragment instance: basically anything that
- // can create more than 1 root nodes.
- if (
- // multi-children template
- frag.childNodes.length > 1 ||
- // non-element template
- replacer.nodeType !== 1 ||
- // single nested component
- tag === 'component' || resolveAsset(options, 'components', tag) || hasBindAttr(replacer, 'is') ||
- // element directive
- resolveAsset(options, 'elementDirectives', tag) ||
- // for block
- replacer.hasAttribute('v-for') ||
- // if block
- replacer.hasAttribute('v-if')) {
- return frag;
- } else {
- options._replacerAttrs = extractAttrs(replacer);
- mergeAttrs(el, replacer);
- return replacer;
- }
- } else {
- el.appendChild(frag);
- return el;
- }
- } else {
- 'development' !== 'production' && warn('Invalid template option: ' + template);
+ // warn invalid mode
+ if ("development" !== 'production' &&
+ mode && mode !== 'in-out' && mode !== 'out-in') {
+ warn(
+ 'invalid <transition> mode: ' + mode,
+ this.$parent
+ );
}
- }
- /**
- * Helper to extract a component container's attributes
- * into a plain object array.
- *
- * @param {Element} el
- * @return {Array}
- */
+ var rawChild = children[0];
- function extractAttrs(el) {
- if (el.nodeType === 1 && el.hasAttributes()) {
- return toArray(el.attributes);
+ // if this is a component root node and the component's
+ // parent container node also has transition, skip.
+ if (hasParentTransition(this.$vnode)) {
+ return rawChild
}
- }
- /**
- * Merge the attributes of two elements, and make sure
- * the class names are merged properly.
- *
- * @param {Element} from
- * @param {Element} to
- */
-
- function mergeAttrs(from, to) {
- var attrs = from.attributes;
- var i = attrs.length;
- var name, value;
- while (i--) {
- name = attrs[i].name;
- value = attrs[i].value;
- if (!to.hasAttribute(name) && !specialCharRE.test(name)) {
- to.setAttribute(name, value);
- } else if (name === 'class' && !parseText(value) && (value = value.trim())) {
- value.split(/\s+/).forEach(function (cls) {
- addClass(to, cls);
- });
+ // apply transition data to child
+ // use getRealChild() to ignore abstract components e.g. keep-alive
+ var child = getRealChild(rawChild);
+ /* istanbul ignore if */
+ if (!child) {
+ return rawChild
+ }
+
+ if (this._leaving) {
+ return placeholder(h, rawChild)
+ }
+
+ var key = child.key = child.key == null || child.isStatic
+ ? ("__v" + (child.tag + this._uid) + "__")
+ : child.key;
+ var data = (child.data || (child.data = {})).transition = extractTransitionData(this);
+ var oldRawChild = this._vnode;
+ var oldChild = getRealChild(oldRawChild);
+
+ // mark v-show
+ // so that the transition module can hand over the control to the directive
+ if (child.data.directives && child.data.directives.some(function (d) { return d.name === 'show'; })) {
+ child.data.show = true;
+ }
+
+ if (oldChild && oldChild.data && oldChild.key !== key) {
+ // replace old child transition data with fresh one
+ // important for dynamic transitions!
+ var oldData = oldChild.data.transition = extend({}, data);
+
+ // handle transition mode
+ if (mode === 'out-in') {
+ // return placeholder node and queue update when leave finishes
+ this._leaving = true;
+ mergeVNodeHook(oldData, 'afterLeave', function () {
+ this$1._leaving = false;
+ this$1.$forceUpdate();
+ }, key);
+ return placeholder(h, rawChild)
+ } else if (mode === 'in-out') {
+ var delayedLeave;
+ var performLeave = function () { delayedLeave(); };
+ mergeVNodeHook(data, 'afterEnter', performLeave, key);
+ mergeVNodeHook(data, 'enterCancelled', performLeave, key);
+ mergeVNodeHook(oldData, 'delayLeave', function (leave) {
+ delayedLeave = leave;
+ }, key);
+ }
+ }
+
+ return rawChild
+ }
+};
+
+/* */
+
+// Provides transition support for list items.
+// supports move transitions using the FLIP technique.
+
+// Because the vdom's children update algorithm is "unstable" - i.e.
+// it doesn't guarantee the relative positioning of removed elements,
+// we force transition-group to update its children into two passes:
+// in the first pass, we remove all nodes that need to be removed,
+// triggering their leaving transition; in the second pass, we insert/move
+// into the final disired state. This way in the second pass removed
+// nodes will remain where they should be.
+
+var props = extend({
+ tag: String,
+ moveClass: String
+}, transitionProps);
+
+delete props.mode;
+
+var TransitionGroup = {
+ props: props,
+
+ render: function render (h) {
+ var tag = this.tag || this.$vnode.data.tag || 'span';
+ var map = Object.create(null);
+ var prevChildren = this.prevChildren = this.children;
+ var rawChildren = this.$slots.default || [];
+ var children = this.children = [];
+ var transitionData = extractTransitionData(this);
+
+ for (var i = 0; i < rawChildren.length; i++) {
+ var c = rawChildren[i];
+ if (c.tag) {
+ if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
+ children.push(c);
+ map[c.key] = c
+ ;(c.data || (c.data = {})).transition = transitionData;
+ } else {
+ var opts = c.componentOptions;
+ var name = opts
+ ? (opts.Ctor.options.name || opts.tag)
+ : c.tag;
+ warn(("<transition-group> children must be keyed: <" + name + ">"));
+ }
}
}
- }
-
- /**
- * Scan and determine slot content distribution.
- * We do this during transclusion instead at compile time so that
- * the distribution is decoupled from the compilation order of
- * the slots.
- *
- * @param {Element|DocumentFragment} template
- * @param {Element} content
- * @param {Vue} vm
- */
- function resolveSlots(vm, content) {
- if (!content) {
- return;
- }
- var contents = vm._slotContents = Object.create(null);
- var el, name;
- for (var i = 0, l = content.children.length; i < l; i++) {
- el = content.children[i];
- /* eslint-disable no-cond-assign */
- if (name = el.getAttribute('slot')) {
- (contents[name] || (contents[name] = [])).push(el);
+ if (prevChildren) {
+ var kept = [];
+ var removed = [];
+ for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {
+ var c$1 = prevChildren[i$1];
+ c$1.data.transition = transitionData;
+ c$1.data.pos = c$1.elm.getBoundingClientRect();
+ if (map[c$1.key]) {
+ kept.push(c$1);
+ } else {
+ removed.push(c$1);
+ }
+ }
+ this.kept = h(tag, null, kept);
+ this.removed = removed;
+ }
+
+ return h(tag, null, children)
+ },
+
+ beforeUpdate: function beforeUpdate () {
+ // force removing pass
+ this.__patch__(
+ this._vnode,
+ this.kept,
+ false, // hydrating
+ true // removeOnly (!important, avoids unnecessary moves)
+ );
+ this._vnode = this.kept;
+ },
+
+ updated: function updated () {
+ var children = this.prevChildren;
+ var moveClass = this.moveClass || (this.name + '-move');
+ if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
+ return
+ }
+
+ // we divide the work into three loops to avoid mixing DOM reads and writes
+ // in each iteration - which helps prevent layout thrashing.
+ children.forEach(callPendingCbs);
+ children.forEach(recordPosition);
+ children.forEach(applyTranslation);
+
+ // force reflow to put everything in position
+ var f = document.body.offsetHeight; // eslint-disable-line
+
+ children.forEach(function (c) {
+ if (c.data.moved) {
+ var el = c.elm;
+ var s = el.style;
+ addTransitionClass(el, moveClass);
+ s.transform = s.WebkitTransform = s.transitionDuration = '';
+ el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
+ if (!e || /transform$/.test(e.propertyName)) {
+ el.removeEventListener(transitionEndEvent, cb);
+ el._moveCb = null;
+ removeTransitionClass(el, moveClass);
+ }
+ });
}
- /* eslint-enable no-cond-assign */
- if ('development' !== 'production' && getBindAttr(el, 'slot')) {
- warn('The "slot" attribute must be static.', vm.$parent);
+ });
+ },
+
+ methods: {
+ hasMove: function hasMove (el, moveClass) {
+ /* istanbul ignore if */
+ if (!hasTransition) {
+ return false
}
- }
- for (name in contents) {
- contents[name] = extractFragment(contents[name], content);
- }
- if (content.hasChildNodes()) {
- var nodes = content.childNodes;
- if (nodes.length === 1 && nodes[0].nodeType === 3 && !nodes[0].data.trim()) {
- return;
+ if (this._hasMove != null) {
+ return this._hasMove
}
- contents['default'] = extractFragment(content.childNodes, content);
+ addTransitionClass(el, moveClass);
+ var info = getTransitionInfo(el);
+ removeTransitionClass(el, moveClass);
+ return (this._hasMove = info.hasTransform)
}
}
+};
- /**
- * Extract qualified content nodes from a node list.
- *
- * @param {NodeList} nodes
- * @return {DocumentFragment}
- */
-
- function extractFragment(nodes, parent) {
- var frag = document.createDocumentFragment();
- nodes = toArray(nodes);
- for (var i = 0, l = nodes.length; i < l; i++) {
- var node = nodes[i];
- if (isTemplate(node) && !node.hasAttribute('v-if') && !node.hasAttribute('v-for')) {
- parent.removeChild(node);
- node = parseTemplate(node, true);
- }
- frag.appendChild(node);
+function callPendingCbs (c) {
+ /* istanbul ignore if */
+ if (c.elm._moveCb) {
+ c.elm._moveCb();
+ }
+ /* istanbul ignore if */
+ if (c.elm._enterCb) {
+ c.elm._enterCb();
+ }
+}
+
+function recordPosition (c) {
+ c.data.newPos = c.elm.getBoundingClientRect();
+}
+
+function applyTranslation (c) {
+ var oldPos = c.data.pos;
+ var newPos = c.data.newPos;
+ var dx = oldPos.left - newPos.left;
+ var dy = oldPos.top - newPos.top;
+ if (dx || dy) {
+ c.data.moved = true;
+ var s = c.elm.style;
+ s.transform = s.WebkitTransform = "translate(" + dx + "px," + dy + "px)";
+ s.transitionDuration = '0s';
+ }
+}
+
+var platformComponents = {
+ Transition: Transition,
+ TransitionGroup: TransitionGroup
+};
+
+/* */
+
+// install platform specific utils
+Vue$3.config.isUnknownElement = isUnknownElement;
+Vue$3.config.isReservedTag = isReservedTag;
+Vue$3.config.getTagNamespace = getTagNamespace;
+Vue$3.config.mustUseProp = mustUseProp;
+
+// install platform runtime directives & components
+extend(Vue$3.options.directives, platformDirectives);
+extend(Vue$3.options.components, platformComponents);
+
+// install platform patch function
+Vue$3.prototype.__patch__ = config._isServer ? noop : patch$1;
+
+// wrap mount
+Vue$3.prototype.$mount = function (
+ el,
+ hydrating
+) {
+ el = el && !config._isServer ? query(el) : undefined;
+ return this._mount(el, hydrating)
+};
+
+// devtools global hook
+/* istanbul ignore next */
+setTimeout(function () {
+ if (config.devtools) {
+ if (devtools) {
+ devtools.emit('init', Vue$3);
+ } else if (
+ "development" !== 'production' &&
+ inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)
+ ) {
+ console.log(
+ 'Download the Vue Devtools for a better development experience:\n' +
+ 'https://github.com/vuejs/vue-devtools'
+ );
}
- return frag;
}
+}, 0);
+/* */
+// check whether current browser encodes a char inside attribute values
+function shouldDecode (content, encoded) {
+ var div = document.createElement('div');
+ div.innerHTML = "<div a=\"" + content + "\">";
+ return div.innerHTML.indexOf(encoded) > 0
+}
- var compiler = Object.freeze({
- compile: compile,
- compileAndLinkProps: compileAndLinkProps,
- compileRoot: compileRoot,
- transclude: transclude,
- resolveSlots: resolveSlots
- });
+// #3663
+// IE encodes newlines inside attribute values while other browsers don't
+var shouldDecodeNewlines = inBrowser ? shouldDecode('\n', '&#10;') : false;
- function stateMixin (Vue) {
- /**
- * Accessor for `$data` property, since setting $data
- * requires observing the new object and updating
- * proxied properties.
- */
-
- Object.defineProperty(Vue.prototype, '$data', {
- get: function get() {
- return this._data;
- },
- set: function set(newData) {
- if (newData !== this._data) {
- this._setData(newData);
- }
- }
- });
+/* */
- /**
- * Setup the scope of an instance, which contains:
- * - observed data
- * - computed properties
- * - user methods
- * - meta properties
- */
-
- Vue.prototype._initState = function () {
- this._initProps();
- this._initMeta();
- this._initMethods();
- this._initData();
- this._initComputed();
- };
+var decoder = document.createElement('div');
- /**
- * Initialize props.
- */
-
- Vue.prototype._initProps = function () {
- var options = this.$options;
- var el = options.el;
- var props = options.props;
- if (props && !el) {
- 'development' !== 'production' && warn('Props will not be compiled if no `el` option is ' + 'provided at instantiation.', this);
- }
- // make sure to convert string selectors into element now
- el = options.el = query(el);
- this._propsUnlinkFn = el && el.nodeType === 1 && props
- // props must be linked in proper scope if inside v-for
- ? compileAndLinkProps(this, el, props, this._scope) : null;
- };
+function decode (html) {
+ decoder.innerHTML = html;
+ return decoder.textContent
+}
- /**
- * Initialize the data.
- */
-
- Vue.prototype._initData = function () {
- var dataFn = this.$options.data;
- var data = this._data = dataFn ? dataFn() : {};
- if (!isPlainObject(data)) {
- data = {};
- 'development' !== 'production' && warn('data functions should return an object.', this);
- }
- var props = this._props;
- // proxy data on instance
- var keys = Object.keys(data);
- var i, key;
- i = keys.length;
- while (i--) {
- key = keys[i];
- // there are two scenarios where we can proxy a data key:
- // 1. it's not already defined as a prop
- // 2. it's provided via a instantiation option AND there are no
- // template prop present
- if (!props || !hasOwn(props, key)) {
- this._proxy(key);
- } else if ('development' !== 'production') {
- warn('Data field "' + key + '" is already defined ' + 'as a prop. To provide default value for a prop, use the "default" ' + 'prop option; if you want to pass prop values to an instantiation ' + 'call, use the "propsData" option.', this);
- }
- }
- // observe data
- observe(data, this);
- };
+/**
+ * Not type-checking this file because it's mostly vendor code.
+ */
- /**
- * Swap the instance's $data. Called in $data's setter.
- *
- * @param {Object} newData
- */
-
- Vue.prototype._setData = function (newData) {
- newData = newData || {};
- var oldData = this._data;
- this._data = newData;
- var keys, key, i;
- // unproxy keys not present in new data
- keys = Object.keys(oldData);
- i = keys.length;
- while (i--) {
- key = keys[i];
- if (!(key in newData)) {
- this._unproxy(key);
- }
- }
- // proxy keys not already proxied,
- // and trigger change for changed values
- keys = Object.keys(newData);
- i = keys.length;
- while (i--) {
- key = keys[i];
- if (!hasOwn(this, key)) {
- // new property
- this._proxy(key);
- }
- }
- oldData.__ob__.removeVm(this);
- observe(newData, this);
- this._digest();
- };
+/*!
+ * HTML Parser By John Resig (ejohn.org)
+ * Modified by Juriy "kangax" Zaytsev
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ */
- /**
- * Proxy a property, so that
- * vm.prop === vm._data.prop
- *
- * @param {String} key
- */
-
- Vue.prototype._proxy = function (key) {
- if (!isReserved(key)) {
- // need to store ref to self here
- // because these getter/setters might
- // be called by child scopes via
- // prototype inheritance.
- var self = this;
- Object.defineProperty(self, key, {
- configurable: true,
- enumerable: true,
- get: function proxyGetter() {
- return self._data[key];
- },
- set: function proxySetter(val) {
- self._data[key] = val;
+// Regular Expressions for parsing tags and attributes
+var singleAttrIdentifier = /([^\s"'<>\/=]+)/;
+var singleAttrAssign = /(?:=)/;
+var singleAttrValues = [
+ // attr value double quotes
+ /"([^"]*)"+/.source,
+ // attr value, single quotes
+ /'([^']*)'+/.source,
+ // attr value, no quotes
+ /([^\s"'=<>`]+)/.source
+];
+var attribute = new RegExp(
+ '^\\s*' + singleAttrIdentifier.source +
+ '(?:\\s*(' + singleAttrAssign.source + ')' +
+ '\\s*(?:' + singleAttrValues.join('|') + '))?'
+);
+
+// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
+// but for Vue templates we can enforce a simple charset
+var ncname = '[a-zA-Z_][\\w\\-\\.]*';
+var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
+var startTagOpen = new RegExp('^<' + qnameCapture);
+var startTagClose = /^\s*(\/?)>/;
+var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
+var doctype = /^<!DOCTYPE [^>]+>/i;
+
+var IS_REGEX_CAPTURING_BROKEN = false;
+'x'.replace(/x(.)?/g, function (m, g) {
+ IS_REGEX_CAPTURING_BROKEN = g === '';
+});
+
+// Special Elements (can contain anything)
+var isSpecialTag = makeMap('script,style', true);
+
+var reCache = {};
+
+var ltRE = /&lt;/g;
+var gtRE = /&gt;/g;
+var nlRE = /&#10;/g;
+var ampRE = /&amp;/g;
+var quoteRE = /&quot;/g;
+
+function decodeAttr (value, shouldDecodeNewlines) {
+ if (shouldDecodeNewlines) {
+ value = value.replace(nlRE, '\n');
+ }
+ return value
+ .replace(ltRE, '<')
+ .replace(gtRE, '>')
+ .replace(ampRE, '&')
+ .replace(quoteRE, '"')
+}
+
+function parseHTML (html, options) {
+ var stack = [];
+ var expectHTML = options.expectHTML;
+ var isUnaryTag$$1 = options.isUnaryTag || no;
+ var index = 0;
+ var last, lastTag;
+ while (html) {
+ last = html;
+ // Make sure we're not in a script or style element
+ if (!lastTag || !isSpecialTag(lastTag)) {
+ var textEnd = html.indexOf('<');
+ if (textEnd === 0) {
+ // Comment:
+ if (/^<!--/.test(html)) {
+ var commentEnd = html.indexOf('-->');
+
+ if (commentEnd >= 0) {
+ advance(commentEnd + 3);
+ continue
}
- });
- }
- };
-
- /**
- * Unproxy a property.
- *
- * @param {String} key
- */
-
- Vue.prototype._unproxy = function (key) {
- if (!isReserved(key)) {
- delete this[key];
- }
- };
-
- /**
- * Force update on every watcher in scope.
- */
+ }
- Vue.prototype._digest = function () {
- for (var i = 0, l = this._watchers.length; i < l; i++) {
- this._watchers[i].update(true); // shallow updates
- }
- };
+ // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
+ if (/^<!\[/.test(html)) {
+ var conditionalEnd = html.indexOf(']>');
- /**
- * Setup computed properties. They are essentially
- * special getter/setters
- */
-
- function noop() {}
- Vue.prototype._initComputed = function () {
- var computed = this.$options.computed;
- if (computed) {
- for (var key in computed) {
- var userDef = computed[key];
- var def = {
- enumerable: true,
- configurable: true
- };
- if (typeof userDef === 'function') {
- def.get = makeComputedGetter(userDef, this);
- def.set = noop;
- } else {
- def.get = userDef.get ? userDef.cache !== false ? makeComputedGetter(userDef.get, this) : bind(userDef.get, this) : noop;
- def.set = userDef.set ? bind(userDef.set, this) : noop;
+ if (conditionalEnd >= 0) {
+ advance(conditionalEnd + 2);
+ continue
}
- Object.defineProperty(this, key, def);
}
- }
- };
- function makeComputedGetter(getter, owner) {
- var watcher = new Watcher(owner, getter, null, {
- lazy: true
- });
- return function computedGetter() {
- if (watcher.dirty) {
- watcher.evaluate();
+ // Doctype:
+ var doctypeMatch = html.match(doctype);
+ if (doctypeMatch) {
+ advance(doctypeMatch[0].length);
+ continue
}
- if (Dep.target) {
- watcher.depend();
- }
- return watcher.value;
- };
- }
- /**
- * Setup instance methods. Methods must be bound to the
- * instance since they might be passed down as a prop to
- * child components.
- */
-
- Vue.prototype._initMethods = function () {
- var methods = this.$options.methods;
- if (methods) {
- for (var key in methods) {
- this[key] = bind(methods[key], this);
+ // End tag:
+ var endTagMatch = html.match(endTag);
+ if (endTagMatch) {
+ var curIndex = index;
+ advance(endTagMatch[0].length);
+ parseEndTag(endTagMatch[0], endTagMatch[1], curIndex, index);
+ continue
}
- }
- };
-
- /**
- * Initialize meta information like $index, $key & $value.
- */
- Vue.prototype._initMeta = function () {
- var metas = this.$options._meta;
- if (metas) {
- for (var key in metas) {
- defineReactive(this, key, metas[key]);
+ // Start tag:
+ var startTagMatch = parseStartTag();
+ if (startTagMatch) {
+ handleStartTag(startTagMatch);
+ continue
}
}
- };
- }
-
- var eventRE = /^v-on:|^@/;
-
- function eventsMixin (Vue) {
- /**
- * Setup the instance's option events & watchers.
- * If the value is a string, we pull it from the
- * instance's methods by name.
- */
-
- Vue.prototype._initEvents = function () {
- var options = this.$options;
- if (options._asComponent) {
- registerComponentEvents(this, options.el);
- }
- registerCallbacks(this, '$on', options.events);
- registerCallbacks(this, '$watch', options.watch);
- };
- /**
- * Register v-on events on a child component
- *
- * @param {Vue} vm
- * @param {Element} el
- */
-
- function registerComponentEvents(vm, el) {
- var attrs = el.attributes;
- var name, value, handler;
- for (var i = 0, l = attrs.length; i < l; i++) {
- name = attrs[i].name;
- if (eventRE.test(name)) {
- name = name.replace(eventRE, '');
- // force the expression into a statement so that
- // it always dynamically resolves the method to call (#2670)
- // kinda ugly hack, but does the job.
- value = attrs[i].value;
- if (isSimplePath(value)) {
- value += '.apply(this, $arguments)';
- }
- handler = (vm._scope || vm._context).$eval(value, true);
- handler._fromParent = true;
- vm.$on(name.replace(eventRE), handler);
- }
+ var text = void 0;
+ if (textEnd >= 0) {
+ text = html.substring(0, textEnd);
+ advance(textEnd);
+ } else {
+ text = html;
+ html = '';
}
- }
- /**
- * Register callbacks for option events and watchers.
- *
- * @param {Vue} vm
- * @param {String} action
- * @param {Object} hash
- */
-
- function registerCallbacks(vm, action, hash) {
- if (!hash) return;
- var handlers, key, i, j;
- for (key in hash) {
- handlers = hash[key];
- if (isArray(handlers)) {
- for (i = 0, j = handlers.length; i < j; i++) {
- register(vm, action, key, handlers[i]);
- }
- } else {
- register(vm, action, key, handlers);
- }
+ if (options.chars) {
+ options.chars(text);
}
+ } else {
+ var stackedTag = lastTag.toLowerCase();
+ var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
+ var endTagLength = 0;
+ var rest = html.replace(reStackedTag, function (all, text, endTag) {
+ endTagLength = endTag.length;
+ if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
+ text = text
+ .replace(/<!--([\s\S]*?)-->/g, '$1')
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1');
+ }
+ if (options.chars) {
+ options.chars(text);
+ }
+ return ''
+ });
+ index += html.length - rest.length;
+ html = rest;
+ parseEndTag('</' + stackedTag + '>', stackedTag, index - endTagLength, index);
}
- /**
- * Helper to register an event/watch callback.
- *
- * @param {Vue} vm
- * @param {String} action
- * @param {String} key
- * @param {Function|String|Object} handler
- * @param {Object} [options]
- */
-
- function register(vm, action, key, handler, options) {
- var type = typeof handler;
- if (type === 'function') {
- vm[action](key, handler, options);
- } else if (type === 'string') {
- var methods = vm.$options.methods;
- var method = methods && methods[handler];
- if (method) {
- vm[action](key, method, options);
- } else {
- 'development' !== 'production' && warn('Unknown method: "' + handler + '" when ' + 'registering callback for ' + action + ': "' + key + '".', vm);
- }
- } else if (handler && type === 'object') {
- register(vm, action, key, handler.handler, handler);
- }
+ if (html === last) {
+ throw new Error('Error parsing template:\n\n' + html)
}
+ }
- /**
- * Setup recursive attached/detached calls
- */
-
- Vue.prototype._initDOMHooks = function () {
- this.$on('hook:attached', onAttached);
- this.$on('hook:detached', onDetached);
- };
+ // Clean up any remaining tags
+ parseEndTag();
- /**
- * Callback to recursively call attached hook on children
- */
+ function advance (n) {
+ index += n;
+ html = html.substring(n);
+ }
- function onAttached() {
- if (!this._isAttached) {
- this._isAttached = true;
- this.$children.forEach(callAttach);
+ function parseStartTag () {
+ var start = html.match(startTagOpen);
+ if (start) {
+ var match = {
+ tagName: start[1],
+ attrs: [],
+ start: index
+ };
+ advance(start[0].length);
+ var end, attr;
+ while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
+ advance(attr[0].length);
+ match.attrs.push(attr);
}
- }
-
- /**
- * Iterator to call attached hook
- *
- * @param {Vue} child
- */
-
- function callAttach(child) {
- if (!child._isAttached && inDoc(child.$el)) {
- child._callHook('attached');
+ if (end) {
+ match.unarySlash = end[1];
+ advance(end[0].length);
+ match.end = index;
+ return match
}
}
+ }
- /**
- * Callback to recursively call detached hook on children
- */
+ function handleStartTag (match) {
+ var tagName = match.tagName;
+ var unarySlash = match.unarySlash;
- function onDetached() {
- if (this._isAttached) {
- this._isAttached = false;
- this.$children.forEach(callDetach);
+ if (expectHTML) {
+ if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
+ parseEndTag('', lastTag);
}
- }
-
- /**
- * Iterator to call detached hook
- *
- * @param {Vue} child
- */
-
- function callDetach(child) {
- if (child._isAttached && !inDoc(child.$el)) {
- child._callHook('detached');
+ if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
+ parseEndTag('', tagName);
}
}
- /**
- * Trigger all handlers for a hook
- *
- * @param {String} hook
- */
+ var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;
- Vue.prototype._callHook = function (hook) {
- this.$emit('pre-hook:' + hook);
- var handlers = this.$options[hook];
- if (handlers) {
- for (var i = 0, j = handlers.length; i < j; i++) {
- handlers[i].call(this);
- }
+ var l = match.attrs.length;
+ var attrs = new Array(l);
+ for (var i = 0; i < l; i++) {
+ var args = match.attrs[i];
+ // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
+ if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
+ if (args[3] === '') { delete args[3]; }
+ if (args[4] === '') { delete args[4]; }
+ if (args[5] === '') { delete args[5]; }
}
- this.$emit('hook:' + hook);
- };
- }
+ var value = args[3] || args[4] || args[5] || '';
+ attrs[i] = {
+ name: args[1],
+ value: decodeAttr(
+ value,
+ options.shouldDecodeNewlines
+ )
+ };
+ }
- function noop$1() {}
+ if (!unary) {
+ stack.push({ tag: tagName, attrs: attrs });
+ lastTag = tagName;
+ unarySlash = '';
+ }
- /**
- * A directive links a DOM element with a piece of data,
- * which is the result of evaluating an expression.
- * It registers a watcher with the expression and calls
- * the DOM update function when a change is triggered.
- *
- * @param {Object} descriptor
- * - {String} name
- * - {Object} def
- * - {String} expression
- * - {Array<Object>} [filters]
- * - {Object} [modifiers]
- * - {Boolean} literal
- * - {String} attr
- * - {String} arg
- * - {String} raw
- * - {String} [ref]
- * - {Array<Object>} [interp]
- * - {Boolean} [hasOneTime]
- * @param {Vue} vm
- * @param {Node} el
- * @param {Vue} [host] - transclusion host component
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - owner fragment
- * @constructor
- */
- function Directive(descriptor, vm, el, host, scope, frag) {
- this.vm = vm;
- this.el = el;
- // copy descriptor properties
- this.descriptor = descriptor;
- this.name = descriptor.name;
- this.expression = descriptor.expression;
- this.arg = descriptor.arg;
- this.modifiers = descriptor.modifiers;
- this.filters = descriptor.filters;
- this.literal = this.modifiers && this.modifiers.literal;
- // private
- this._locked = false;
- this._bound = false;
- this._listeners = null;
- // link context
- this._host = host;
- this._scope = scope;
- this._frag = frag;
- // store directives on node in dev mode
- if ('development' !== 'production' && this.el) {
- this.el._vue_directives = this.el._vue_directives || [];
- this.el._vue_directives.push(this);
+ if (options.start) {
+ options.start(tagName, attrs, unary, match.start, match.end);
}
}
- /**
- * Initialize the directive, mixin definition properties,
- * setup the watcher, call definition bind() and update()
- * if present.
- */
-
- Directive.prototype._bind = function () {
- var name = this.name;
- var descriptor = this.descriptor;
+ function parseEndTag (tag, tagName, start, end) {
+ var pos;
+ if (start == null) { start = index; }
+ if (end == null) { end = index; }
- // remove attribute
- if ((name !== 'cloak' || this.vm._isCompiled) && this.el && this.el.removeAttribute) {
- var attr = descriptor.attr || 'v-' + name;
- this.el.removeAttribute(attr);
- }
-
- // copy def properties
- var def = descriptor.def;
- if (typeof def === 'function') {
- this.update = def;
+ // Find the closest opened tag of the same type
+ if (tagName) {
+ var needle = tagName.toLowerCase();
+ for (pos = stack.length - 1; pos >= 0; pos--) {
+ if (stack[pos].tag.toLowerCase() === needle) {
+ break
+ }
+ }
} else {
- extend(this, def);
+ // If no tag name is provided, clean shop
+ pos = 0;
}
- // setup directive params
- this._setupParams();
-
- // initial bind
- if (this.bind) {
- this.bind();
- }
- this._bound = true;
+ if (pos >= 0) {
+ // Close all the open elements, up the stack
+ for (var i = stack.length - 1; i >= pos; i--) {
+ if (options.end) {
+ options.end(stack[i].tag, start, end);
+ }
+ }
- if (this.literal) {
- this.update && this.update(descriptor.raw);
- } else if ((this.expression || this.modifiers) && (this.update || this.twoWay) && !this._checkStatement()) {
- // wrapped updater for context
- var dir = this;
- if (this.update) {
- this._update = function (val, oldVal) {
- if (!dir._locked) {
- dir.update(val, oldVal);
- }
- };
- } else {
- this._update = noop$1;
+ // Remove the open elements from the stack
+ stack.length = pos;
+ lastTag = pos && stack[pos - 1].tag;
+ } else if (tagName.toLowerCase() === 'br') {
+ if (options.start) {
+ options.start(tagName, [], true, start, end);
}
- var preProcess = this._preProcess ? bind(this._preProcess, this) : null;
- var postProcess = this._postProcess ? bind(this._postProcess, this) : null;
- var watcher = this._watcher = new Watcher(this.vm, this.expression, this._update, // callback
- {
- filters: this.filters,
- twoWay: this.twoWay,
- deep: this.deep,
- preProcess: preProcess,
- postProcess: postProcess,
- scope: this._scope
- });
- // v-model with inital inline value need to sync back to
- // model instead of update to DOM on init. They would
- // set the afterBind hook to indicate that.
- if (this.afterBind) {
- this.afterBind();
- } else if (this.update) {
- this.update(watcher.value);
+ } else if (tagName.toLowerCase() === 'p') {
+ if (options.start) {
+ options.start(tagName, [], false, start, end);
}
- }
- };
-
- /**
- * Setup all param attributes, e.g. track-by,
- * transition-mode, etc...
- */
-
- Directive.prototype._setupParams = function () {
- if (!this.params) {
- return;
- }
- var params = this.params;
- // swap the params array with a fresh object.
- this.params = Object.create(null);
- var i = params.length;
- var key, val, mappedKey;
- while (i--) {
- key = hyphenate(params[i]);
- mappedKey = camelize(key);
- val = getBindAttr(this.el, key);
- if (val != null) {
- // dynamic
- this._setupParamWatcher(mappedKey, val);
- } else {
- // static
- val = getAttr(this.el, key);
- if (val != null) {
- this.params[mappedKey] = val === '' ? true : val;
- }
+ if (options.end) {
+ options.end(tagName, start, end);
}
}
- };
+ }
+}
- /**
- * Setup a watcher for a dynamic param.
- *
- * @param {String} key
- * @param {String} expression
- */
+/* */
- Directive.prototype._setupParamWatcher = function (key, expression) {
- var self = this;
- var called = false;
- var unwatch = (this._scope || this.vm).$watch(expression, function (val, oldVal) {
- self.params[key] = val;
- // since we are in immediate mode,
- // only call the param change callbacks if this is not the first update.
- if (called) {
- var cb = self.paramWatchers && self.paramWatchers[key];
- if (cb) {
- cb.call(self, val, oldVal);
- }
+function parseFilters (exp) {
+ var inSingle = false;
+ var inDouble = false;
+ var curly = 0;
+ var square = 0;
+ var paren = 0;
+ var lastFilterIndex = 0;
+ var c, prev, i, expression, filters;
+
+ for (i = 0; i < exp.length; i++) {
+ prev = c;
+ c = exp.charCodeAt(i);
+ if (inSingle) {
+ // check single quote
+ if (c === 0x27 && prev !== 0x5C) { inSingle = !inSingle; }
+ } else if (inDouble) {
+ // check double quote
+ if (c === 0x22 && prev !== 0x5C) { inDouble = !inDouble; }
+ } else if (
+ c === 0x7C && // pipe
+ exp.charCodeAt(i + 1) !== 0x7C &&
+ exp.charCodeAt(i - 1) !== 0x7C &&
+ !curly && !square && !paren
+ ) {
+ if (expression === undefined) {
+ // first filter, end of expression
+ lastFilterIndex = i + 1;
+ expression = exp.slice(0, i).trim();
} else {
- called = true;
+ pushFilter();
}
- }, {
- immediate: true,
- user: false
- });(this._paramUnwatchFns || (this._paramUnwatchFns = [])).push(unwatch);
- };
-
- /**
- * Check if the directive is a function caller
- * and if the expression is a callable one. If both true,
- * we wrap up the expression and use it as the event
- * handler.
- *
- * e.g. on-click="a++"
- *
- * @return {Boolean}
- */
-
- Directive.prototype._checkStatement = function () {
- var expression = this.expression;
- if (expression && this.acceptStatement && !isSimplePath(expression)) {
- var fn = parseExpression(expression).get;
- var scope = this._scope || this.vm;
- var handler = function handler(e) {
- scope.$event = e;
- fn.call(scope, scope);
- scope.$event = null;
- };
- if (this.filters) {
- handler = scope._applyFilters(handler, null, this.filters);
+ } else {
+ switch (c) {
+ case 0x22: inDouble = true; break // "
+ case 0x27: inSingle = true; break // '
+ case 0x28: paren++; break // (
+ case 0x29: paren--; break // )
+ case 0x5B: square++; break // [
+ case 0x5D: square--; break // ]
+ case 0x7B: curly++; break // {
+ case 0x7D: curly--; break // }
}
- this.update(handler);
- return true;
- }
- };
-
- /**
- * Set the corresponding value with the setter.
- * This should only be used in two-way directives
- * e.g. v-model.
- *
- * @param {*} value
- * @public
- */
-
- Directive.prototype.set = function (value) {
- /* istanbul ignore else */
- if (this.twoWay) {
- this._withLock(function () {
- this._watcher.set(value);
- });
- } else if ('development' !== 'production') {
- warn('Directive.set() can only be used inside twoWay' + 'directives.');
}
- };
-
- /**
- * Execute a function while preventing that function from
- * triggering updates on this directive instance.
- *
- * @param {Function} fn
- */
-
- Directive.prototype._withLock = function (fn) {
- var self = this;
- self._locked = true;
- fn.call(self);
- nextTick(function () {
- self._locked = false;
- });
- };
-
- /**
- * Convenience method that attaches a DOM event listener
- * to the directive element and autometically tears it down
- * during unbind.
- *
- * @param {String} event
- * @param {Function} handler
- * @param {Boolean} [useCapture]
- */
+ }
- Directive.prototype.on = function (event, handler, useCapture) {
- on(this.el, event, handler, useCapture);(this._listeners || (this._listeners = [])).push([event, handler]);
- };
+ if (expression === undefined) {
+ expression = exp.slice(0, i).trim();
+ } else if (lastFilterIndex !== 0) {
+ pushFilter();
+ }
- /**
- * Teardown the watcher and call unbind.
- */
+ function pushFilter () {
+ (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
+ lastFilterIndex = i + 1;
+ }
- Directive.prototype._teardown = function () {
- if (this._bound) {
- this._bound = false;
- if (this.unbind) {
- this.unbind();
- }
- if (this._watcher) {
- this._watcher.teardown();
- }
- var listeners = this._listeners;
- var i;
- if (listeners) {
- i = listeners.length;
- while (i--) {
- off(this.el, listeners[i][0], listeners[i][1]);
- }
- }
- var unwatchFns = this._paramUnwatchFns;
- if (unwatchFns) {
- i = unwatchFns.length;
- while (i--) {
- unwatchFns[i]();
- }
- }
- if ('development' !== 'production' && this.el) {
- this.el._vue_directives.$remove(this);
- }
- this.vm = this.el = this._watcher = this._listeners = null;
+ if (filters) {
+ for (i = 0; i < filters.length; i++) {
+ expression = wrapFilter(expression, filters[i]);
}
- };
+ }
- function lifecycleMixin (Vue) {
- /**
- * Update v-ref for component.
- *
- * @param {Boolean} remove
- */
-
- Vue.prototype._updateRef = function (remove) {
- var ref = this.$options._ref;
- if (ref) {
- var refs = (this._scope || this._context).$refs;
- if (remove) {
- if (refs[ref] === this) {
- refs[ref] = null;
- }
- } else {
- refs[ref] = this;
- }
- }
- };
+ return expression
+}
- /**
- * Transclude, compile and link element.
- *
- * If a pre-compiled linker is available, that means the
- * passed in element will be pre-transcluded and compiled
- * as well - all we need to do is to call the linker.
- *
- * Otherwise we need to call transclude/compile/link here.
- *
- * @param {Element} el
- */
-
- Vue.prototype._compile = function (el) {
- var options = this.$options;
-
- // transclude and init element
- // transclude can potentially replace original
- // so we need to keep reference; this step also injects
- // the template and caches the original attributes
- // on the container node and replacer node.
- var original = el;
- el = transclude(el, options);
- this._initElement(el);
-
- // handle v-pre on root node (#2026)
- if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {
- return;
- }
-
- // root is always compiled per-instance, because
- // container attrs and props can be different every time.
- var contextOptions = this._context && this._context.$options;
- var rootLinker = compileRoot(el, options, contextOptions);
-
- // resolve slot distribution
- resolveSlots(this, options._content);
-
- // compile and link the rest
- var contentLinkFn;
- var ctor = this.constructor;
- // component compilation can be cached
- // as long as it's not using inline-template
- if (options._linkerCachable) {
- contentLinkFn = ctor.linker;
- if (!contentLinkFn) {
- contentLinkFn = ctor.linker = compile(el, options);
- }
+function wrapFilter (exp, filter) {
+ var i = filter.indexOf('(');
+ if (i < 0) {
+ // _f: resolveFilter
+ return ("_f(\"" + filter + "\")(" + exp + ")")
+ } else {
+ var name = filter.slice(0, i);
+ var args = filter.slice(i + 1);
+ return ("_f(\"" + name + "\")(" + exp + "," + args)
+ }
+}
+
+/* */
+
+var defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g;
+var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
+
+var buildRegex = cached(function (delimiters) {
+ var open = delimiters[0].replace(regexEscapeRE, '\\$&');
+ var close = delimiters[1].replace(regexEscapeRE, '\\$&');
+ return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
+});
+
+function parseText (
+ text,
+ delimiters
+) {
+ var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
+ if (!tagRE.test(text)) {
+ return
+ }
+ var tokens = [];
+ var lastIndex = tagRE.lastIndex = 0;
+ var match, index;
+ while ((match = tagRE.exec(text))) {
+ index = match.index;
+ // push text token
+ if (index > lastIndex) {
+ tokens.push(JSON.stringify(text.slice(lastIndex, index)));
+ }
+ // tag token
+ var exp = parseFilters(match[1].trim());
+ tokens.push(("_s(" + exp + ")"));
+ lastIndex = index + match[0].length;
+ }
+ if (lastIndex < text.length) {
+ tokens.push(JSON.stringify(text.slice(lastIndex)));
+ }
+ return tokens.join('+')
+}
+
+/* */
+
+function baseWarn (msg) {
+ console.error(("[Vue parser]: " + msg));
+}
+
+function pluckModuleFunction (
+ modules,
+ key
+) {
+ return modules
+ ? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })
+ : []
+}
+
+function addProp (el, name, value) {
+ (el.props || (el.props = [])).push({ name: name, value: value });
+}
+
+function addAttr (el, name, value) {
+ (el.attrs || (el.attrs = [])).push({ name: name, value: value });
+}
+
+function addDirective (
+ el,
+ name,
+ rawName,
+ value,
+ arg,
+ modifiers
+) {
+ (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
+}
+
+function addHandler (
+ el,
+ name,
+ value,
+ modifiers,
+ important
+) {
+ // check capture modifier
+ if (modifiers && modifiers.capture) {
+ delete modifiers.capture;
+ name = '!' + name; // mark the event as captured
+ }
+ var events;
+ if (modifiers && modifiers.native) {
+ delete modifiers.native;
+ events = el.nativeEvents || (el.nativeEvents = {});
+ } else {
+ events = el.events || (el.events = {});
+ }
+ var newHandler = { value: value, modifiers: modifiers };
+ var handlers = events[name];
+ /* istanbul ignore if */
+ if (Array.isArray(handlers)) {
+ important ? handlers.unshift(newHandler) : handlers.push(newHandler);
+ } else if (handlers) {
+ events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
+ } else {
+ events[name] = newHandler;
+ }
+}
+
+function getBindingAttr (
+ el,
+ name,
+ getStatic
+) {
+ var dynamicValue =
+ getAndRemoveAttr(el, ':' + name) ||
+ getAndRemoveAttr(el, 'v-bind:' + name);
+ if (dynamicValue != null) {
+ return dynamicValue
+ } else if (getStatic !== false) {
+ var staticValue = getAndRemoveAttr(el, name);
+ if (staticValue != null) {
+ return JSON.stringify(staticValue)
+ }
+ }
+}
+
+function getAndRemoveAttr (el, name) {
+ var val;
+ if ((val = el.attrsMap[name]) != null) {
+ var list = el.attrsList;
+ for (var i = 0, l = list.length; i < l; i++) {
+ if (list[i].name === name) {
+ list.splice(i, 1);
+ break
+ }
+ }
+ }
+ return val
+}
+
+/* */
+
+var dirRE = /^v-|^@|^:/;
+var forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/;
+var forIteratorRE = /\(([^,]*),([^,]*)(?:,([^,]*))?\)/;
+var bindRE = /^:|^v-bind:/;
+var onRE = /^@|^v-on:/;
+var argRE = /:(.*)$/;
+var modifierRE = /\.[^\.]+/g;
+var specialNewlineRE = /\u2028|\u2029/g;
+
+var decodeHTMLCached = cached(decode);
+
+// configurable state
+var warn$1;
+var platformGetTagNamespace;
+var platformMustUseProp;
+var platformIsPreTag;
+var preTransforms;
+var transforms;
+var postTransforms;
+var delimiters;
+
+/**
+ * Convert HTML string to AST.
+ */
+function parse (
+ template,
+ options
+) {
+ warn$1 = options.warn || baseWarn;
+ platformGetTagNamespace = options.getTagNamespace || no;
+ platformMustUseProp = options.mustUseProp || no;
+ platformIsPreTag = options.isPreTag || no;
+ preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
+ transforms = pluckModuleFunction(options.modules, 'transformNode');
+ postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
+ delimiters = options.delimiters;
+ var stack = [];
+ var preserveWhitespace = options.preserveWhitespace !== false;
+ var root;
+ var currentParent;
+ var inVPre = false;
+ var inPre = false;
+ var warned = false;
+ parseHTML(template, {
+ expectHTML: options.expectHTML,
+ isUnaryTag: options.isUnaryTag,
+ shouldDecodeNewlines: options.shouldDecodeNewlines,
+ start: function start (tag, attrs, unary) {
+ // check namespace.
+ // inherit parent ns if there is one
+ var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
+
+ // handle IE svg bug
+ /* istanbul ignore if */
+ if (options.isIE && ns === 'svg') {
+ attrs = guardIESVGBug(attrs);
}
- // link phase
- // make sure to link root with prop scope!
- var rootUnlinkFn = rootLinker(this, el, this._scope);
- var contentUnlinkFn = contentLinkFn ? contentLinkFn(this, el) : compile(el, options)(this, el);
-
- // register composite unlink function
- // to be called during instance destruction
- this._unlinkFn = function () {
- rootUnlinkFn();
- // passing destroying: true to avoid searching and
- // splicing the directives
- contentUnlinkFn(true);
+ var element = {
+ type: 1,
+ tag: tag,
+ attrsList: attrs,
+ attrsMap: makeAttrsMap(attrs, options.isIE),
+ parent: currentParent,
+ children: []
};
-
- // finally replace original
- if (options.replace) {
- replace(original, el);
+ if (ns) {
+ element.ns = ns;
}
- this._isCompiled = true;
- this._callHook('compiled');
- };
-
- /**
- * Initialize instance element. Called in the public
- * $mount() method.
- *
- * @param {Element} el
- */
-
- Vue.prototype._initElement = function (el) {
- if (isFragment(el)) {
- this._isFragment = true;
- this.$el = this._fragmentStart = el.firstChild;
- this._fragmentEnd = el.lastChild;
- // set persisted text anchors to empty
- if (this._fragmentStart.nodeType === 3) {
- this._fragmentStart.data = this._fragmentEnd.data = '';
- }
- this._fragment = el;
- } else {
- this.$el = el;
+ if ("client" !== 'server' && isForbiddenTag(element)) {
+ element.forbidden = true;
+ "development" !== 'production' && warn$1(
+ 'Templates should only be responsible for mapping the state to the ' +
+ 'UI. Avoid placing tags with side-effects in your templates, such as ' +
+ "<" + tag + ">."
+ );
}
- this.$el.__vue__ = this;
- this._callHook('beforeCompile');
- };
- /**
- * Create and bind a directive to an element.
- *
- * @param {Object} descriptor - parsed directive descriptor
- * @param {Node} node - target node
- * @param {Vue} [host] - transclusion host component
- * @param {Object} [scope] - v-for scope
- * @param {Fragment} [frag] - owner fragment
- */
-
- Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
- this._directives.push(new Directive(descriptor, this, node, host, scope, frag));
- };
-
- /**
- * Teardown an instance, unobserves the data, unbind all the
- * directives, turn off all the event listeners, etc.
- *
- * @param {Boolean} remove - whether to remove the DOM node.
- * @param {Boolean} deferCleanup - if true, defer cleanup to
- * be called later
- */
-
- Vue.prototype._destroy = function (remove, deferCleanup) {
- if (this._isBeingDestroyed) {
- if (!deferCleanup) {
- this._cleanup();
- }
- return;
+ // apply pre-transforms
+ for (var i = 0; i < preTransforms.length; i++) {
+ preTransforms[i](element, options);
}
- var destroyReady;
- var pendingRemoval;
-
- var self = this;
- // Cleanup should be called either synchronously or asynchronoysly as
- // callback of this.$remove(), or if remove and deferCleanup are false.
- // In any case it should be called after all other removing, unbinding and
- // turning of is done
- var cleanupIfPossible = function cleanupIfPossible() {
- if (destroyReady && !pendingRemoval && !deferCleanup) {
- self._cleanup();
+ if (!inVPre) {
+ processPre(element);
+ if (element.pre) {
+ inVPre = true;
}
- };
-
- // remove DOM element
- if (remove && this.$el) {
- pendingRemoval = true;
- this.$remove(function () {
- pendingRemoval = false;
- cleanupIfPossible();
- });
- }
-
- this._callHook('beforeDestroy');
- this._isBeingDestroyed = true;
- var i;
- // remove self from parent. only necessary
- // if parent is not being destroyed as well.
- var parent = this.$parent;
- if (parent && !parent._isBeingDestroyed) {
- parent.$children.$remove(this);
- // unregister ref (remove: true)
- this._updateRef(true);
- }
- // destroy all children.
- i = this.$children.length;
- while (i--) {
- this.$children[i].$destroy();
}
- // teardown props
- if (this._propsUnlinkFn) {
- this._propsUnlinkFn();
+ if (platformIsPreTag(element.tag)) {
+ inPre = true;
}
- // teardown all directives. this also tearsdown all
- // directive-owned watchers.
- if (this._unlinkFn) {
- this._unlinkFn();
- }
- i = this._watchers.length;
- while (i--) {
- this._watchers[i].teardown();
- }
- // remove reference to self on $el
- if (this.$el) {
- this.$el.__vue__ = null;
- }
-
- destroyReady = true;
- cleanupIfPossible();
- };
-
- /**
- * Clean up to ensure garbage collection.
- * This is called after the leave transition if there
- * is any.
- */
-
- Vue.prototype._cleanup = function () {
- if (this._isDestroyed) {
- return;
- }
- // remove self from owner fragment
- // do it in cleanup so that we can call $destroy with
- // defer right when a fragment is about to be removed.
- if (this._frag) {
- this._frag.children.$remove(this);
- }
- // remove reference from data ob
- // frozen object may not have observer.
- if (this._data && this._data.__ob__) {
- this._data.__ob__.removeVm(this);
- }
- // Clean up references to private properties and other
- // instances. preserve reference to _data so that proxy
- // accessors still work. The only potential side effect
- // here is that mutating the instance after it's destroyed
- // may affect the state of other components that are still
- // observing the same object, but that seems to be a
- // reasonable responsibility for the user rather than
- // always throwing an error on them.
- this.$el = this.$parent = this.$root = this.$children = this._watchers = this._context = this._scope = this._directives = null;
- // call the last hook...
- this._isDestroyed = true;
- this._callHook('destroyed');
- // turn off all instance listeners.
- this.$off();
- };
- }
-
- function miscMixin (Vue) {
- /**
- * Apply a list of filter (descriptors) to a value.
- * Using plain for loops here because this will be called in
- * the getter of any watcher with filters so it is very
- * performance sensitive.
- *
- * @param {*} value
- * @param {*} [oldValue]
- * @param {Array} filters
- * @param {Boolean} write
- * @return {*}
- */
-
- Vue.prototype._applyFilters = function (value, oldValue, filters, write) {
- var filter, fn, args, arg, offset, i, l, j, k;
- for (i = 0, l = filters.length; i < l; i++) {
- filter = filters[write ? l - i - 1 : i];
- fn = resolveAsset(this.$options, 'filters', filter.name, true);
- if (!fn) continue;
- fn = write ? fn.write : fn.read || fn;
- if (typeof fn !== 'function') continue;
- args = write ? [value, oldValue] : [value];
- offset = write ? 2 : 1;
- if (filter.args) {
- for (j = 0, k = filter.args.length; j < k; j++) {
- arg = filter.args[j];
- args[j + offset] = arg.dynamic ? this.$get(arg.value) : arg.value;
+ if (inVPre) {
+ processRawAttrs(element);
+ } else {
+ processFor(element);
+ processIf(element);
+ processOnce(element);
+ processKey(element);
+
+ // determine whether this is a plain element after
+ // removing structural attributes
+ element.plain = !element.key && !attrs.length;
+
+ processRef(element);
+ processSlot(element);
+ processComponent(element);
+ for (var i$1 = 0; i$1 < transforms.length; i$1++) {
+ transforms[i$1](element, options);
+ }
+ processAttrs(element);
+ }
+
+ function checkRootConstraints (el) {
+ {
+ if (el.tag === 'slot' || el.tag === 'template') {
+ warn$1(
+ "Cannot use <" + (el.tag) + "> as component root element because it may " +
+ 'contain multiple nodes:\n' + template
+ );
+ }
+ if (el.attrsMap.hasOwnProperty('v-for')) {
+ warn$1(
+ 'Cannot use v-for on stateful component root element because ' +
+ 'it renders multiple elements:\n' + template
+ );
}
}
- value = fn.apply(this, args);
}
- return value;
- };
- /**
- * Resolve a component, depending on whether the component
- * is defined normally or using an async factory function.
- * Resolves synchronously if already resolved, otherwise
- * resolves asynchronously and caches the resolved
- * constructor on the factory.
- *
- * @param {String|Function} value
- * @param {Function} cb
- */
-
- Vue.prototype._resolveComponent = function (value, cb) {
- var factory;
- if (typeof value === 'function') {
- factory = value;
- } else {
- factory = resolveAsset(this.$options, 'components', value, true);
- }
- /* istanbul ignore if */
- if (!factory) {
- return;
- }
- // async component factory
- if (!factory.options) {
- if (factory.resolved) {
- // cached
- cb(factory.resolved);
- } else if (factory.requested) {
- // pool callbacks
- factory.pendingCallbacks.push(cb);
+ // tree management
+ if (!root) {
+ root = element;
+ checkRootConstraints(root);
+ } else if ("development" !== 'production' && !stack.length && !warned) {
+ // allow 2 root elements with v-if and v-else
+ if (root.if && element.else) {
+ checkRootConstraints(element);
+ root.elseBlock = element;
} else {
- factory.requested = true;
- var cbs = factory.pendingCallbacks = [cb];
- factory.call(this, function resolve(res) {
- if (isPlainObject(res)) {
- res = Vue.extend(res);
- }
- // cache resolved
- factory.resolved = res;
- // invoke callbacks
- for (var i = 0, l = cbs.length; i < l; i++) {
- cbs[i](res);
- }
- }, function reject(reason) {
- 'development' !== 'production' && warn('Failed to resolve async component' + (typeof value === 'string' ? ': ' + value : '') + '. ' + (reason ? '\nReason: ' + reason : ''));
- });
+ warned = true;
+ warn$1(
+ ("Component template should contain exactly one root element:\n\n" + template)
+ );
}
- } else {
- // normal component
- cb(factory);
}
- };
- }
-
- var filterRE$1 = /[^|]\|[^|]/;
-
- function dataAPI (Vue) {
- /**
- * Get the value from an expression on this vm.
- *
- * @param {String} exp
- * @param {Boolean} [asStatement]
- * @return {*}
- */
-
- Vue.prototype.$get = function (exp, asStatement) {
- var res = parseExpression(exp);
- if (res) {
- if (asStatement) {
- var self = this;
- return function statementHandler() {
- self.$arguments = toArray(arguments);
- var result = res.get.call(self, self);
- self.$arguments = null;
- return result;
- };
+ if (currentParent && !element.forbidden) {
+ if (element.else) {
+ processElse(element, currentParent);
} else {
- try {
- return res.get.call(this, this);
- } catch (e) {}
+ currentParent.children.push(element);
+ element.parent = currentParent;
}
}
- };
-
- /**
- * Set the value from an expression on this vm.
- * The expression must be a valid left-hand
- * expression in an assignment.
- *
- * @param {String} exp
- * @param {*} val
- */
-
- Vue.prototype.$set = function (exp, val) {
- var res = parseExpression(exp, true);
- if (res && res.set) {
- res.set.call(this, this, val);
+ if (!unary) {
+ currentParent = element;
+ stack.push(element);
}
- };
-
- /**
- * Delete a property on the VM
- *
- * @param {String} key
- */
-
- Vue.prototype.$delete = function (key) {
- del(this._data, key);
- };
-
- /**
- * Watch an expression, trigger callback when its
- * value changes.
- *
- * @param {String|Function} expOrFn
- * @param {Function} cb
- * @param {Object} [options]
- * - {Boolean} deep
- * - {Boolean} immediate
- * @return {Function} - unwatchFn
- */
-
- Vue.prototype.$watch = function (expOrFn, cb, options) {
- var vm = this;
- var parsed;
- if (typeof expOrFn === 'string') {
- parsed = parseDirective(expOrFn);
- expOrFn = parsed.expression;
- }
- var watcher = new Watcher(vm, expOrFn, cb, {
- deep: options && options.deep,
- sync: options && options.sync,
- filters: parsed && parsed.filters,
- user: !options || options.user !== false
- });
- if (options && options.immediate) {
- cb.call(vm, watcher.value);
+ // apply post-transforms
+ for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
+ postTransforms[i$2](element, options);
}
- return function unwatchFn() {
- watcher.teardown();
- };
- };
+ },
- /**
- * Evaluate a text directive, including filters.
- *
- * @param {String} text
- * @param {Boolean} [asStatement]
- * @return {String}
- */
-
- Vue.prototype.$eval = function (text, asStatement) {
- // check for filters.
- if (filterRE$1.test(text)) {
- var dir = parseDirective(text);
- // the filter regex check might give false positive
- // for pipes inside strings, so it's possible that
- // we don't get any filters here
- var val = this.$get(dir.expression, asStatement);
- return dir.filters ? this._applyFilters(val, null, dir.filters) : val;
- } else {
- // no filter
- return this.$get(text, asStatement);
+ end: function end () {
+ // remove trailing whitespace
+ var element = stack[stack.length - 1];
+ var lastNode = element.children[element.children.length - 1];
+ if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
+ element.children.pop();
}
- };
-
- /**
- * Interpolate a piece of template text.
- *
- * @param {String} text
- * @return {String}
- */
-
- Vue.prototype.$interpolate = function (text) {
- var tokens = parseText(text);
- var vm = this;
- if (tokens) {
- if (tokens.length === 1) {
- return vm.$eval(tokens[0].value) + '';
- } else {
- return tokens.map(function (token) {
- return token.tag ? vm.$eval(token.value) : token.value;
- }).join('');
- }
- } else {
- return text;
+ // pop stack
+ stack.length -= 1;
+ currentParent = stack[stack.length - 1];
+ // check pre state
+ if (element.pre) {
+ inVPre = false;
}
- };
+ if (platformIsPreTag(element.tag)) {
+ inPre = false;
+ }
+ },
- /**
- * Log instance data as a plain JS object
- * so that it is easier to inspect in console.
- * This method assumes console is available.
- *
- * @param {String} [path]
- */
-
- Vue.prototype.$log = function (path) {
- var data = path ? getPath(this._data, path) : this._data;
- if (data) {
- data = clean(data);
- }
- // include computed fields
- if (!path) {
- var key;
- for (key in this.$options.computed) {
- data[key] = clean(this[key]);
- }
- if (this._props) {
- for (key in this._props) {
- data[key] = clean(this[key]);
- }
+ chars: function chars (text) {
+ if (!currentParent) {
+ if ("development" !== 'production' && !warned && text === template) {
+ warned = true;
+ warn$1(
+ 'Component template requires a root element, rather than just text:\n\n' + template
+ );
+ }
+ return
+ }
+ text = inPre || text.trim()
+ ? decodeHTMLCached(text)
+ // only preserve whitespace if its not right after a starting tag
+ : preserveWhitespace && currentParent.children.length ? ' ' : '';
+ if (text) {
+ var expression;
+ if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
+ currentParent.children.push({
+ type: 2,
+ expression: expression,
+ text: text
+ });
+ } else {
+ // #3895 special character
+ text = text.replace(specialNewlineRE, '');
+ currentParent.children.push({
+ type: 3,
+ text: text
+ });
}
}
- console.log(data);
- };
-
- /**
- * "clean" a getter/setter converted object into a plain
- * object copy.
- *
- * @param {Object} - obj
- * @return {Object}
- */
-
- function clean(obj) {
- return JSON.parse(JSON.stringify(obj));
}
- }
-
- function domAPI (Vue) {
- /**
- * Convenience on-instance nextTick. The callback is
- * auto-bound to the instance, and this avoids component
- * modules having to rely on the global Vue.
- *
- * @param {Function} fn
- */
-
- Vue.prototype.$nextTick = function (fn) {
- nextTick(fn, this);
- };
-
- /**
- * Append instance to target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$appendTo = function (target, cb, withTransition) {
- return insert(this, target, cb, withTransition, append, appendWithTransition);
- };
-
- /**
- * Prepend instance to target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$prependTo = function (target, cb, withTransition) {
- target = query(target);
- if (target.hasChildNodes()) {
- this.$before(target.firstChild, cb, withTransition);
- } else {
- this.$appendTo(target, cb, withTransition);
- }
- return this;
- };
-
- /**
- * Insert instance before target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$before = function (target, cb, withTransition) {
- return insert(this, target, cb, withTransition, beforeWithCb, beforeWithTransition);
- };
-
- /**
- * Insert instance after target
- *
- * @param {Node} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$after = function (target, cb, withTransition) {
- target = query(target);
- if (target.nextSibling) {
- this.$before(target.nextSibling, cb, withTransition);
- } else {
- this.$appendTo(target.parentNode, cb, withTransition);
- }
- return this;
- };
-
- /**
- * Remove instance from DOM
- *
- * @param {Function} [cb]
- * @param {Boolean} [withTransition] - defaults to true
- */
-
- Vue.prototype.$remove = function (cb, withTransition) {
- if (!this.$el.parentNode) {
- return cb && cb();
- }
- var inDocument = this._isAttached && inDoc(this.$el);
- // if we are not in document, no need to check
- // for transitions
- if (!inDocument) withTransition = false;
- var self = this;
- var realCb = function realCb() {
- if (inDocument) self._callHook('detached');
- if (cb) cb();
+ });
+ return root
+}
+
+function processPre (el) {
+ if (getAndRemoveAttr(el, 'v-pre') != null) {
+ el.pre = true;
+ }
+}
+
+function processRawAttrs (el) {
+ var l = el.attrsList.length;
+ if (l) {
+ var attrs = el.attrs = new Array(l);
+ for (var i = 0; i < l; i++) {
+ attrs[i] = {
+ name: el.attrsList[i].name,
+ value: JSON.stringify(el.attrsList[i].value)
};
- if (this._isFragment) {
- removeNodeRange(this._fragmentStart, this._fragmentEnd, this, this._fragment, realCb);
- } else {
- var op = withTransition === false ? removeWithCb : removeWithTransition;
- op(this.$el, this, realCb);
- }
- return this;
- };
-
- /**
- * Shared DOM insertion function.
- *
- * @param {Vue} vm
- * @param {Element} target
- * @param {Function} [cb]
- * @param {Boolean} [withTransition]
- * @param {Function} op1 - op for non-transition insert
- * @param {Function} op2 - op for transition insert
- * @return vm
- */
-
- function insert(vm, target, cb, withTransition, op1, op2) {
- target = query(target);
- var targetIsDetached = !inDoc(target);
- var op = withTransition === false || targetIsDetached ? op1 : op2;
- var shouldCallHook = !targetIsDetached && !vm._isAttached && !inDoc(vm.$el);
- if (vm._isFragment) {
- mapNodeRange(vm._fragmentStart, vm._fragmentEnd, function (node) {
- op(node, target, vm);
- });
- cb && cb();
- } else {
- op(vm.$el, target, vm, cb);
- }
- if (shouldCallHook) {
- vm._callHook('attached');
- }
- return vm;
- }
-
- /**
- * Check for selectors
- *
- * @param {String|Element} el
- */
-
- function query(el) {
- return typeof el === 'string' ? document.querySelector(el) : el;
}
-
- /**
- * Append operation that takes a callback.
- *
- * @param {Node} el
- * @param {Node} target
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
-
- function append(el, target, vm, cb) {
- target.appendChild(el);
- if (cb) cb();
+ } else if (!el.pre) {
+ // non root node in pre blocks with no attributes
+ el.plain = true;
+ }
+}
+
+function processKey (el) {
+ var exp = getBindingAttr(el, 'key');
+ if (exp) {
+ if ("development" !== 'production' && el.tag === 'template') {
+ warn$1("<template> cannot be keyed. Place the key on real elements instead.");
+ }
+ el.key = exp;
+ }
+}
+
+function processRef (el) {
+ var ref = getBindingAttr(el, 'ref');
+ if (ref) {
+ el.ref = ref;
+ el.refInFor = checkInFor(el);
+ }
+}
+
+function processFor (el) {
+ var exp;
+ if ((exp = getAndRemoveAttr(el, 'v-for'))) {
+ var inMatch = exp.match(forAliasRE);
+ if (!inMatch) {
+ "development" !== 'production' && warn$1(
+ ("Invalid v-for expression: " + exp)
+ );
+ return
+ }
+ el.for = inMatch[2].trim();
+ var alias = inMatch[1].trim();
+ var iteratorMatch = alias.match(forIteratorRE);
+ if (iteratorMatch) {
+ el.alias = iteratorMatch[1].trim();
+ el.iterator1 = iteratorMatch[2].trim();
+ if (iteratorMatch[3]) {
+ el.iterator2 = iteratorMatch[3].trim();
+ }
+ } else {
+ el.alias = alias;
}
+ }
+}
- /**
- * InsertBefore operation that takes a callback.
- *
- * @param {Node} el
- * @param {Node} target
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
+function processIf (el) {
+ var exp = getAndRemoveAttr(el, 'v-if');
+ if (exp) {
+ el.if = exp;
+ }
+ if (getAndRemoveAttr(el, 'v-else') != null) {
+ el.else = true;
+ }
+}
- function beforeWithCb(el, target, vm, cb) {
- before(el, target);
- if (cb) cb();
- }
+function processElse (el, parent) {
+ var prev = findPrevElement(parent.children);
+ if (prev && prev.if) {
+ prev.elseBlock = el;
+ } else {
+ warn$1(
+ ("v-else used on element <" + (el.tag) + "> without corresponding v-if.")
+ );
+ }
+}
- /**
- * Remove operation that takes a callback.
- *
- * @param {Node} el
- * @param {Vue} vm - unused
- * @param {Function} [cb]
- */
+function processOnce (el) {
+ var once = getAndRemoveAttr(el, 'v-once');
+ if (once != null) {
+ el.once = true;
+ }
+}
- function removeWithCb(el, vm, cb) {
- remove(el);
- if (cb) cb();
+function processSlot (el) {
+ if (el.tag === 'slot') {
+ el.slotName = getBindingAttr(el, 'name');
+ } else {
+ var slotTarget = getBindingAttr(el, 'slot');
+ if (slotTarget) {
+ el.slotTarget = slotTarget;
}
}
+}
- function eventsAPI (Vue) {
- /**
- * Listen on the given `event` with `fn`.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$on = function (event, fn) {
- (this._events[event] || (this._events[event] = [])).push(fn);
- modifyListenerCount(this, event, 1);
- return this;
- };
-
- /**
- * Adds an `event` listener that will be invoked a single
- * time then automatically removed.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$once = function (event, fn) {
- var self = this;
- function on() {
- self.$off(event, on);
- fn.apply(this, arguments);
- }
- on.fn = fn;
- this.$on(event, on);
- return this;
- };
+function processComponent (el) {
+ var binding;
+ if ((binding = getBindingAttr(el, 'is'))) {
+ el.component = binding;
+ }
+ if (getAndRemoveAttr(el, 'inline-template') != null) {
+ el.inlineTemplate = true;
+ }
+}
- /**
- * Remove the given callback for `event` or all
- * registered callbacks.
- *
- * @param {String} event
- * @param {Function} fn
- */
-
- Vue.prototype.$off = function (event, fn) {
- var cbs;
- // all
- if (!arguments.length) {
- if (this.$parent) {
- for (event in this._events) {
- cbs = this._events[event];
- if (cbs) {
- modifyListenerCount(this, event, -cbs.length);
- }
- }
- }
- this._events = {};
- return this;
- }
- // specific event
- cbs = this._events[event];
- if (!cbs) {
- return this;
- }
- if (arguments.length === 1) {
- modifyListenerCount(this, event, -cbs.length);
- this._events[event] = null;
- return this;
- }
- // specific handler
- var cb;
- var i = cbs.length;
- while (i--) {
- cb = cbs[i];
- if (cb === fn || cb.fn === fn) {
- modifyListenerCount(this, event, -1);
- cbs.splice(i, 1);
- break;
+function processAttrs (el) {
+ var list = el.attrsList;
+ var i, l, name, rawName, value, arg, modifiers, isProp;
+ for (i = 0, l = list.length; i < l; i++) {
+ name = rawName = list[i].name;
+ value = list[i].value;
+ if (dirRE.test(name)) {
+ // mark element as dynamic
+ el.hasBindings = true;
+ // modifiers
+ modifiers = parseModifiers(name);
+ if (modifiers) {
+ name = name.replace(modifierRE, '');
+ }
+ if (bindRE.test(name)) { // v-bind
+ name = name.replace(bindRE, '');
+ if (modifiers && modifiers.prop) {
+ isProp = true;
+ name = camelize(name);
+ if (name === 'innerHtml') { name = 'innerHTML'; }
+ }
+ if (isProp || platformMustUseProp(name)) {
+ addProp(el, name, value);
+ } else {
+ addAttr(el, name, value);
}
- }
- return this;
- };
-
- /**
- * Trigger an event on self.
- *
- * @param {String|Object} event
- * @return {Boolean} shouldPropagate
- */
-
- Vue.prototype.$emit = function (event) {
- var isSource = typeof event === 'string';
- event = isSource ? event : event.name;
- var cbs = this._events[event];
- var shouldPropagate = isSource || !cbs;
- if (cbs) {
- cbs = cbs.length > 1 ? toArray(cbs) : cbs;
- // this is a somewhat hacky solution to the question raised
- // in #2102: for an inline component listener like <comp @test="doThis">,
- // the propagation handling is somewhat broken. Therefore we
- // need to treat these inline callbacks differently.
- var hasParentCbs = isSource && cbs.some(function (cb) {
- return cb._fromParent;
- });
- if (hasParentCbs) {
- shouldPropagate = false;
+ } else if (onRE.test(name)) { // v-on
+ name = name.replace(onRE, '');
+ addHandler(el, name, value, modifiers);
+ } else { // normal directives
+ name = name.replace(dirRE, '');
+ // parse arg
+ var argMatch = name.match(argRE);
+ if (argMatch && (arg = argMatch[1])) {
+ name = name.slice(0, -(arg.length + 1));
}
- var args = toArray(arguments, 1);
- for (var i = 0, l = cbs.length; i < l; i++) {
- var cb = cbs[i];
- var res = cb.apply(this, args);
- if (res === true && (!hasParentCbs || cb._fromParent)) {
- shouldPropagate = true;
- }
+ addDirective(el, name, rawName, value, arg, modifiers);
+ if ("development" !== 'production' && name === 'model') {
+ checkForAliasModel(el, value);
}
}
- return shouldPropagate;
- };
-
- /**
- * Recursively broadcast an event to all children instances.
- *
- * @param {String|Object} event
- * @param {...*} additional arguments
- */
-
- Vue.prototype.$broadcast = function (event) {
- var isSource = typeof event === 'string';
- event = isSource ? event : event.name;
- // if no child has registered for this event,
- // then there's no need to broadcast.
- if (!this._eventsCount[event]) return;
- var children = this.$children;
- var args = toArray(arguments);
- if (isSource) {
- // use object event to indicate non-source emit
- // on children
- args[0] = { name: event, source: this };
- }
- for (var i = 0, l = children.length; i < l; i++) {
- var child = children[i];
- var shouldPropagate = child.$emit.apply(child, args);
- if (shouldPropagate) {
- child.$broadcast.apply(child, args);
+ } else {
+ // literal attribute
+ {
+ var expression = parseText(value, delimiters);
+ if (expression) {
+ warn$1(
+ name + "=\"" + value + "\": " +
+ 'Interpolation inside attributes has been deprecated. ' +
+ 'Use v-bind or the colon shorthand instead.'
+ );
}
}
- return this;
- };
-
- /**
- * Recursively propagate an event up the parent chain.
- *
- * @param {String} event
- * @param {...*} additional arguments
- */
-
- Vue.prototype.$dispatch = function (event) {
- var shouldPropagate = this.$emit.apply(this, arguments);
- if (!shouldPropagate) return;
- var parent = this.$parent;
- var args = toArray(arguments);
- // use object event to indicate non-source emit
- // on parents
- args[0] = { name: event, source: this };
- while (parent) {
- shouldPropagate = parent.$emit.apply(parent, args);
- parent = shouldPropagate ? parent.$parent : null;
- }
- return this;
- };
-
- /**
- * Modify the listener counts on all parents.
- * This bookkeeping allows $broadcast to return early when
- * no child has listened to a certain event.
- *
- * @param {Vue} vm
- * @param {String} event
- * @param {Number} count
- */
-
- var hookRE = /^hook:/;
- function modifyListenerCount(vm, event, count) {
- var parent = vm.$parent;
- // hooks do not get broadcasted so no need
- // to do bookkeeping for them
- if (!parent || !count || hookRE.test(event)) return;
- while (parent) {
- parent._eventsCount[event] = (parent._eventsCount[event] || 0) + count;
- parent = parent.$parent;
- }
- }
- }
-
- function lifecycleAPI (Vue) {
- /**
- * Set instance target element and kick off the compilation
- * process. The passed in `el` can be a selector string, an
- * existing Element, or a DocumentFragment (for block
- * instances).
- *
- * @param {Element|DocumentFragment|string} el
- * @public
- */
-
- Vue.prototype.$mount = function (el) {
- if (this._isCompiled) {
- 'development' !== 'production' && warn('$mount() should be called only once.', this);
- return;
- }
- el = query(el);
- if (!el) {
- el = document.createElement('div');
- }
- this._compile(el);
- this._initDOMHooks();
- if (inDoc(this.$el)) {
- this._callHook('attached');
- ready.call(this);
- } else {
- this.$once('hook:attached', ready);
- }
- return this;
- };
-
- /**
- * Mark an instance as ready.
- */
-
- function ready() {
- this._isAttached = true;
- this._isReady = true;
- this._callHook('ready');
+ addAttr(el, name, JSON.stringify(value));
}
-
- /**
- * Teardown the instance, simply delegate to the internal
- * _destroy.
- *
- * @param {Boolean} remove
- * @param {Boolean} deferCleanup
- */
-
- Vue.prototype.$destroy = function (remove, deferCleanup) {
- this._destroy(remove, deferCleanup);
- };
-
- /**
- * Partially compile a piece of DOM and return a
- * decompile function.
- *
- * @param {Element|DocumentFragment} el
- * @param {Vue} [host]
- * @param {Object} [scope]
- * @param {Fragment} [frag]
- * @return {Function}
- */
-
- Vue.prototype.$compile = function (el, host, scope, frag) {
- return compile(el, this.$options, true)(this, el, host, scope, frag);
- };
}
+}
- /**
- * The exposed Vue constructor.
- *
- * API conventions:
- * - public API methods/properties are prefixed with `$`
- * - internal methods/properties are prefixed with `_`
- * - non-prefixed properties are assumed to be proxied user
- * data.
- *
- * @constructor
- * @param {Object} [options]
- * @public
- */
-
- function Vue(options) {
- this._init(options);
+function checkInFor (el) {
+ var parent = el;
+ while (parent) {
+ if (parent.for !== undefined) {
+ return true
+ }
+ parent = parent.parent;
}
+ return false
+}
- // install internals
- initMixin(Vue);
- stateMixin(Vue);
- eventsMixin(Vue);
- lifecycleMixin(Vue);
- miscMixin(Vue);
-
- // install instance APIs
- dataAPI(Vue);
- domAPI(Vue);
- eventsAPI(Vue);
- lifecycleAPI(Vue);
-
- var slot = {
-
- priority: SLOT,
- params: ['name'],
-
- bind: function bind() {
- // this was resolved during component transclusion
- var name = this.params.name || 'default';
- var content = this.vm._slotContents && this.vm._slotContents[name];
- if (!content || !content.hasChildNodes()) {
- this.fallback();
- } else {
- this.compile(content.cloneNode(true), this.vm._context, this.vm);
- }
- },
-
- compile: function compile(content, context, host) {
- if (content && context) {
- if (this.el.hasChildNodes() && content.childNodes.length === 1 && content.childNodes[0].nodeType === 1 && content.childNodes[0].hasAttribute('v-if')) {
- // if the inserted slot has v-if
- // inject fallback content as the v-else
- var elseBlock = document.createElement('template');
- elseBlock.setAttribute('v-else', '');
- elseBlock.innerHTML = this.el.innerHTML;
- // the else block should be compiled in child scope
- elseBlock._context = this.vm;
- content.appendChild(elseBlock);
- }
- var scope = host ? host._scope : this._scope;
- this.unlink = context.$compile(content, host, scope, this._frag);
- }
- if (content) {
- replace(this.el, content);
+function parseModifiers (name) {
+ var match = name.match(modifierRE);
+ if (match) {
+ var ret = {};
+ match.forEach(function (m) { ret[m.slice(1)] = true; });
+ return ret
+ }
+}
+
+function makeAttrsMap (attrs, isIE) {
+ var map = {};
+ for (var i = 0, l = attrs.length; i < l; i++) {
+ if ("development" !== 'production' && map[attrs[i].name] && !isIE) {
+ warn$1('duplicate attribute: ' + attrs[i].name);
+ }
+ map[attrs[i].name] = attrs[i].value;
+ }
+ return map
+}
+
+function findPrevElement (children) {
+ var i = children.length;
+ while (i--) {
+ if (children[i].tag) { return children[i] }
+ }
+}
+
+function isForbiddenTag (el) {
+ return (
+ el.tag === 'style' ||
+ (el.tag === 'script' && (
+ !el.attrsMap.type ||
+ el.attrsMap.type === 'text/javascript'
+ ))
+ )
+}
+
+var ieNSBug = /^xmlns:NS\d+/;
+var ieNSPrefix = /^NS\d+:/;
+
+/* istanbul ignore next */
+function guardIESVGBug (attrs) {
+ var res = [];
+ for (var i = 0; i < attrs.length; i++) {
+ var attr = attrs[i];
+ if (!ieNSBug.test(attr.name)) {
+ attr.name = attr.name.replace(ieNSPrefix, '');
+ res.push(attr);
+ }
+ }
+ return res
+}
+
+function checkForAliasModel (el, value) {
+ var _el = el;
+ while (_el) {
+ if (_el.for && _el.alias === value) {
+ warn$1(
+ "<" + (el.tag) + " v-model=\"" + value + "\">: " +
+ "You are binding v-model directly to a v-for iteration alias. " +
+ "This will not be able to modify the v-for source array because " +
+ "writing to the alias is like modifying a function local variable. " +
+ "Consider using an array of objects and use v-model on an object property instead."
+ );
+ }
+ _el = _el.parent;
+ }
+}
+
+/* */
+
+var isStaticKey;
+var isPlatformReservedTag;
+
+var genStaticKeysCached = cached(genStaticKeys$1);
+
+/**
+ * Goal of the optimizier: walk the generated template AST tree
+ * and detect sub-trees that are purely static, i.e. parts of
+ * the DOM that never needs to change.
+ *
+ * Once we detect these sub-trees, we can:
+ *
+ * 1. Hoist them into constants, so that we no longer need to
+ * create fresh nodes for them on each re-render;
+ * 2. Completely skip them in the patching process.
+ */
+function optimize (root, options) {
+ if (!root) { return }
+ isStaticKey = genStaticKeysCached(options.staticKeys || '');
+ isPlatformReservedTag = options.isReservedTag || (function () { return false; });
+ // first pass: mark all non-static nodes.
+ markStatic(root);
+ // second pass: mark static roots.
+ markStaticRoots(root, false);
+}
+
+function genStaticKeys$1 (keys) {
+ return makeMap(
+ 'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
+ (keys ? ',' + keys : '')
+ )
+}
+
+function markStatic (node) {
+ node.static = isStatic(node);
+ if (node.type === 1) {
+ for (var i = 0, l = node.children.length; i < l; i++) {
+ var child = node.children[i];
+ markStatic(child);
+ if (!child.static) {
+ node.static = false;
+ }
+ }
+ }
+}
+
+function markStaticRoots (node, isInFor) {
+ if (node.type === 1) {
+ if (node.once || node.static) {
+ node.staticRoot = true;
+ node.staticInFor = isInFor;
+ return
+ }
+ if (node.children) {
+ for (var i = 0, l = node.children.length; i < l; i++) {
+ markStaticRoots(node.children[i], isInFor || !!node.for);
+ }
+ }
+ }
+}
+
+function isStatic (node) {
+ if (node.type === 2) { // expression
+ return false
+ }
+ if (node.type === 3) { // text
+ return true
+ }
+ return !!(node.pre || (
+ !node.hasBindings && // no dynamic bindings
+ !node.if && !node.for && // not v-if or v-for or v-else
+ !isBuiltInTag(node.tag) && // not a built-in
+ isPlatformReservedTag(node.tag) && // not a component
+ !isDirectChildOfTemplateFor(node) &&
+ Object.keys(node).every(isStaticKey)
+ ))
+}
+
+function isDirectChildOfTemplateFor (node) {
+ while (node.parent) {
+ node = node.parent;
+ if (node.tag !== 'template') {
+ return false
+ }
+ if (node.for) {
+ return true
+ }
+ }
+ return false
+}
+
+/* */
+
+var simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/;
+
+// keyCode aliases
+var keyCodes = {
+ esc: 27,
+ tab: 9,
+ enter: 13,
+ space: 32,
+ up: 38,
+ left: 37,
+ right: 39,
+ down: 40,
+ 'delete': [8, 46]
+};
+
+var modifierCode = {
+ stop: '$event.stopPropagation();',
+ prevent: '$event.preventDefault();',
+ self: 'if($event.target !== $event.currentTarget)return;'
+};
+
+function genHandlers (events, native) {
+ var res = native ? 'nativeOn:{' : 'on:{';
+ for (var name in events) {
+ res += "\"" + name + "\":" + (genHandler(events[name])) + ",";
+ }
+ return res.slice(0, -1) + '}'
+}
+
+function genHandler (
+ handler
+) {
+ if (!handler) {
+ return 'function(){}'
+ } else if (Array.isArray(handler)) {
+ return ("[" + (handler.map(genHandler).join(',')) + "]")
+ } else if (!handler.modifiers) {
+ return simplePathRE.test(handler.value)
+ ? handler.value
+ : ("function($event){" + (handler.value) + "}")
+ } else {
+ var code = '';
+ var keys = [];
+ for (var key in handler.modifiers) {
+ if (modifierCode[key]) {
+ code += modifierCode[key];
} else {
- remove(this.el);
- }
- },
-
- fallback: function fallback() {
- this.compile(extractContent(this.el, true), this.vm);
- },
-
- unbind: function unbind() {
- if (this.unlink) {
- this.unlink();
+ keys.push(key);
}
}
- };
-
- var partial = {
-
- priority: PARTIAL,
-
- params: ['name'],
-
- // watch changes to name for dynamic partials
- paramWatchers: {
- name: function name(value) {
- vIf.remove.call(this);
- if (value) {
- this.insert(value);
- }
- }
- },
+ if (keys.length) {
+ code = genKeyFilter(keys) + code;
+ }
+ var handlerCode = simplePathRE.test(handler.value)
+ ? handler.value + '($event)'
+ : handler.value;
+ return 'function($event){' + code + handlerCode + '}'
+ }
+}
- bind: function bind() {
- this.anchor = createAnchor('v-partial');
- replace(this.el, this.anchor);
- this.insert(this.params.name);
- },
+function genKeyFilter (keys) {
+ var code = keys.length === 1
+ ? normalizeKeyCode(keys[0])
+ : Array.prototype.concat.apply([], keys.map(normalizeKeyCode));
+ if (Array.isArray(code)) {
+ return ("if(" + (code.map(function (c) { return ("$event.keyCode!==" + c); }).join('&&')) + ")return;")
+ } else {
+ return ("if($event.keyCode!==" + code + ")return;")
+ }
+}
- insert: function insert(id) {
- var partial = resolveAsset(this.vm.$options, 'partials', id, true);
- if (partial) {
- this.factory = new FragmentFactory(this.vm, partial);
- vIf.insert.call(this);
- }
- },
+function normalizeKeyCode (key) {
+ return (
+ parseInt(key, 10) || // number keyCode
+ keyCodes[key] || // built-in alias
+ ("_k(" + (JSON.stringify(key)) + ")") // custom alias
+ )
+}
- unbind: function unbind() {
- if (this.frag) {
- this.frag.destroy();
- }
- }
- };
+/* */
- var elementDirectives = {
- slot: slot,
- partial: partial
+function bind$2 (el, dir) {
+ el.wrapData = function (code) {
+ return ("_b(" + code + "," + (dir.value) + (dir.modifiers && dir.modifiers.prop ? ',true' : '') + ")")
};
+}
+
+var baseDirectives = {
+ bind: bind$2,
+ cloak: noop
+};
+
+/* */
+
+// configurable state
+var warn$2;
+var transforms$1;
+var dataGenFns;
+var platformDirectives$1;
+var staticRenderFns;
+var currentOptions;
+
+function generate (
+ ast,
+ options
+) {
+ // save previous staticRenderFns so generate calls can be nested
+ var prevStaticRenderFns = staticRenderFns;
+ var currentStaticRenderFns = staticRenderFns = [];
+ currentOptions = options;
+ warn$2 = options.warn || baseWarn;
+ transforms$1 = pluckModuleFunction(options.modules, 'transformCode');
+ dataGenFns = pluckModuleFunction(options.modules, 'genData');
+ platformDirectives$1 = options.directives || {};
+ var code = ast ? genElement(ast) : '_h("div")';
+ staticRenderFns = prevStaticRenderFns;
+ return {
+ render: ("with(this){return " + code + "}"),
+ staticRenderFns: currentStaticRenderFns
+ }
+}
+
+function genElement (el) {
+ if (el.staticRoot && !el.staticProcessed) {
+ // hoist static sub-trees out
+ el.staticProcessed = true;
+ staticRenderFns.push(("with(this){return " + (genElement(el)) + "}"));
+ return ("_m(" + (staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
+ } else if (el.for && !el.forProcessed) {
+ return genFor(el)
+ } else if (el.if && !el.ifProcessed) {
+ return genIf(el)
+ } else if (el.tag === 'template' && !el.slotTarget) {
+ return genChildren(el) || 'void 0'
+ } else if (el.tag === 'slot') {
+ return genSlot(el)
+ } else {
+ // component or element
+ var code;
+ if (el.component) {
+ code = genComponent(el);
+ } else {
+ var data = genData(el);
+ var children = el.inlineTemplate ? null : genChildren(el);
+ code = "_h('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
+ }
+ // module transforms
+ for (var i = 0; i < transforms$1.length; i++) {
+ code = transforms$1[i](el, code);
+ }
+ return code
+ }
+}
+
+function genIf (el) {
+ var exp = el.if;
+ el.ifProcessed = true; // avoid recursion
+ return ("(" + exp + ")?" + (genElement(el)) + ":" + (genElse(el)))
+}
+
+function genElse (el) {
+ return el.elseBlock
+ ? genElement(el.elseBlock)
+ : '_e()'
+}
+
+function genFor (el) {
+ var exp = el.for;
+ var alias = el.alias;
+ var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
+ var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
+ el.forProcessed = true; // avoid recursion
+ return "_l((" + exp + ")," +
+ "function(" + alias + iterator1 + iterator2 + "){" +
+ "return " + (genElement(el)) +
+ '})'
+}
+
+function genData (el) {
+ if (el.plain) {
+ return
+ }
+
+ var data = '{';
+
+ // directives first.
+ // directives may mutate the el's other properties before they are generated.
+ var dirs = genDirectives(el);
+ if (dirs) { data += dirs + ','; }
+
+ // key
+ if (el.key) {
+ data += "key:" + (el.key) + ",";
+ }
+ // ref
+ if (el.ref) {
+ data += "ref:" + (el.ref) + ",";
+ }
+ if (el.refInFor) {
+ data += "refInFor:true,";
+ }
+ // record original tag name for components using "is" attribute
+ if (el.component) {
+ data += "tag:\"" + (el.tag) + "\",";
+ }
+ // slot target
+ if (el.slotTarget) {
+ data += "slot:" + (el.slotTarget) + ",";
+ }
+ // module data generation functions
+ for (var i = 0; i < dataGenFns.length; i++) {
+ data += dataGenFns[i](el);
+ }
+ // attributes
+ if (el.attrs) {
+ data += "attrs:{" + (genProps(el.attrs)) + "},";
+ }
+ // DOM props
+ if (el.props) {
+ data += "domProps:{" + (genProps(el.props)) + "},";
+ }
+ // event handlers
+ if (el.events) {
+ data += (genHandlers(el.events)) + ",";
+ }
+ if (el.nativeEvents) {
+ data += (genHandlers(el.nativeEvents, true)) + ",";
+ }
+ // inline-template
+ if (el.inlineTemplate) {
+ var ast = el.children[0];
+ if ("development" !== 'production' && (
+ el.children.length > 1 || ast.type !== 1
+ )) {
+ warn$2('Inline-template components must have exactly one child element.');
+ }
+ if (ast.type === 1) {
+ var inlineRenderFns = generate(ast, currentOptions);
+ data += "inlineTemplate:{render:function(){" + (inlineRenderFns.render) + "},staticRenderFns:[" + (inlineRenderFns.staticRenderFns.map(function (code) { return ("function(){" + code + "}"); }).join(',')) + "]}";
+ }
+ }
+ data = data.replace(/,$/, '') + '}';
+ // v-bind data wrap
+ if (el.wrapData) {
+ data = el.wrapData(data);
+ }
+ return data
+}
+
+function genDirectives (el) {
+ var dirs = el.directives;
+ if (!dirs) { return }
+ var res = 'directives:[';
+ var hasRuntime = false;
+ var i, l, dir, needRuntime;
+ for (i = 0, l = dirs.length; i < l; i++) {
+ dir = dirs[i];
+ needRuntime = true;
+ var gen = platformDirectives$1[dir.name] || baseDirectives[dir.name];
+ if (gen) {
+ // compile-time directive that manipulates AST.
+ // returns true if it also needs a runtime counterpart.
+ needRuntime = !!gen(el, dir, warn$2);
+ }
+ if (needRuntime) {
+ hasRuntime = true;
+ res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:\"" + (dir.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
+ }
+ }
+ if (hasRuntime) {
+ return res.slice(0, -1) + ']'
+ }
+}
- var convertArray = vFor._postProcess;
-
- /**
- * Limit filter for arrays
- *
- * @param {Number} n
- * @param {Number} offset (Decimal expected)
- */
-
- function limitBy(arr, n, offset) {
- offset = offset ? parseInt(offset, 10) : 0;
- n = toNumber(n);
- return typeof n === 'number' ? arr.slice(offset, offset + n) : arr;
+function genChildren (el) {
+ if (el.children.length) {
+ return '[' + el.children.map(genNode).join(',') + ']'
}
+}
- /**
- * Filter filter for arrays
- *
- * @param {String} search
- * @param {String} [delimiter]
- * @param {String} ...dataKeys
- */
-
- function filterBy(arr, search, delimiter) {
- arr = convertArray(arr);
- if (search == null) {
- return arr;
- }
- if (typeof search === 'function') {
- return arr.filter(search);
- }
- // cast to lowercase string
- search = ('' + search).toLowerCase();
- // allow optional `in` delimiter
- // because why not
- var n = delimiter === 'in' ? 3 : 2;
- // extract and flatten keys
- var keys = Array.prototype.concat.apply([], toArray(arguments, n));
- var res = [];
- var item, key, val, j;
- for (var i = 0, l = arr.length; i < l; i++) {
- item = arr[i];
- val = item && item.$value || item;
- j = keys.length;
- if (j) {
- while (j--) {
- key = keys[j];
- if (key === '$key' && contains(item.$key, search) || contains(getPath(val, key), search)) {
- res.push(item);
- break;
+function genNode (node) {
+ if (node.type === 1) {
+ return genElement(node)
+ } else {
+ return genText(node)
+ }
+}
+
+function genText (text) {
+ return text.type === 2
+ ? text.expression // no need for () because already wrapped in _s()
+ : JSON.stringify(text.text)
+}
+
+function genSlot (el) {
+ var slotName = el.slotName || '"default"';
+ var children = genChildren(el);
+ return children
+ ? ("_t(" + slotName + "," + children + ")")
+ : ("_t(" + slotName + ")")
+}
+
+function genComponent (el) {
+ var children = el.inlineTemplate ? null : genChildren(el);
+ return ("_h(" + (el.component) + "," + (genData(el)) + (children ? ("," + children) : '') + ")")
+}
+
+function genProps (props) {
+ var res = '';
+ for (var i = 0; i < props.length; i++) {
+ var prop = props[i];
+ res += "\"" + (prop.name) + "\":" + (prop.value) + ",";
+ }
+ return res.slice(0, -1)
+}
+
+/* */
+
+/**
+ * Compile a template.
+ */
+function compile$1 (
+ template,
+ options
+) {
+ var ast = parse(template.trim(), options);
+ optimize(ast, options);
+ var code = generate(ast, options);
+ return {
+ ast: ast,
+ render: code.render,
+ staticRenderFns: code.staticRenderFns
+ }
+}
+
+/* */
+
+// operators like typeof, instanceof and in are allowed
+var prohibitedKeywordRE = new RegExp('\\b' + (
+ 'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +
+ 'super,throw,while,yield,delete,export,import,return,switch,default,' +
+ 'extends,finally,continue,debugger,function,arguments'
+).split(',').join('\\b|\\b') + '\\b');
+// check valid identifier for v-for
+var identRE = /[A-Za-z_$][\w$]*/;
+// strip strings in expressions
+var stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g;
+
+// detect problematic expressions in a template
+function detectErrors (ast) {
+ var errors = [];
+ if (ast) {
+ checkNode(ast, errors);
+ }
+ return errors
+}
+
+function checkNode (node, errors) {
+ if (node.type === 1) {
+ for (var name in node.attrsMap) {
+ if (dirRE.test(name)) {
+ var value = node.attrsMap[name];
+ if (value) {
+ if (name === 'v-for') {
+ checkFor(node, ("v-for=\"" + value + "\""), errors);
+ } else {
+ checkExpression(value, (name + "=\"" + value + "\""), errors);
}
}
- } else if (contains(item, search)) {
- res.push(item);
}
}
- return res;
+ if (node.children) {
+ for (var i = 0; i < node.children.length; i++) {
+ checkNode(node.children[i], errors);
+ }
+ }
+ } else if (node.type === 2) {
+ checkExpression(node.expression, node.text, errors);
}
+}
- /**
- * Filter filter for arrays
- *
- * @param {String|Array<String>|Function} ...sortKeys
- * @param {Number} [order]
- */
-
- function orderBy(arr) {
- var comparator = null;
- var sortKeys = undefined;
- arr = convertArray(arr);
+function checkFor (node, text, errors) {
+ checkExpression(node.for || '', text, errors);
+ checkIdentifier(node.alias, 'v-for alias', text, errors);
+ checkIdentifier(node.iterator1, 'v-for iterator', text, errors);
+ checkIdentifier(node.iterator2, 'v-for iterator', text, errors);
+}
- // determine order (last argument)
- var args = toArray(arguments, 1);
- var order = args[args.length - 1];
- if (typeof order === 'number') {
- order = order < 0 ? -1 : 1;
- args = args.length > 1 ? args.slice(0, -1) : args;
- } else {
- order = 1;
- }
+function checkIdentifier (ident, type, text, errors) {
+ if (typeof ident === 'string' && !identRE.test(ident)) {
+ errors.push(("- invalid " + type + " \"" + ident + "\" in expression: " + text));
+ }
+}
- // determine sortKeys & comparator
- var firstArg = args[0];
- if (!firstArg) {
- return arr;
- } else if (typeof firstArg === 'function') {
- // custom comparator
- comparator = function (a, b) {
- return firstArg(a, b) * order;
- };
+function checkExpression (exp, text, errors) {
+ try {
+ new Function(("return " + exp));
+ } catch (e) {
+ var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);
+ if (keywordMatch) {
+ errors.push(
+ "- avoid using JavaScript keyword as property name: " +
+ "\"" + (keywordMatch[0]) + "\" in expression " + text
+ );
} else {
- // string keys. flatten first
- sortKeys = Array.prototype.concat.apply([], args);
- comparator = function (a, b, i) {
- i = i || 0;
- return i >= sortKeys.length - 1 ? baseCompare(a, b, i) : baseCompare(a, b, i) || comparator(a, b, i + 1);
- };
- }
-
- function baseCompare(a, b, sortKeyIndex) {
- var sortKey = sortKeys[sortKeyIndex];
- if (sortKey) {
- if (sortKey !== '$key') {
- if (isObject(a) && '$value' in a) a = a.$value;
- if (isObject(b) && '$value' in b) b = b.$value;
- }
- a = isObject(a) ? getPath(a, sortKey) : a;
- b = isObject(b) ? getPath(b, sortKey) : b;
+ errors.push(("- invalid expression: " + text));
+ }
+ }
+}
+
+/* */
+
+function transformNode (el, options) {
+ var warn = options.warn || baseWarn;
+ var staticClass = getAndRemoveAttr(el, 'class');
+ if ("development" !== 'production' && staticClass) {
+ var expression = parseText(staticClass, options.delimiters);
+ if (expression) {
+ warn(
+ "class=\"" + staticClass + "\": " +
+ 'Interpolation inside attributes has been deprecated. ' +
+ 'Use v-bind or the colon shorthand instead.'
+ );
+ }
+ }
+ if (staticClass) {
+ el.staticClass = JSON.stringify(staticClass);
+ }
+ var classBinding = getBindingAttr(el, 'class', false /* getStatic */);
+ if (classBinding) {
+ el.classBinding = classBinding;
+ }
+}
+
+function genData$1 (el) {
+ var data = '';
+ if (el.staticClass) {
+ data += "staticClass:" + (el.staticClass) + ",";
+ }
+ if (el.classBinding) {
+ data += "class:" + (el.classBinding) + ",";
+ }
+ return data
+}
+
+var klass$1 = {
+ staticKeys: ['staticClass'],
+ transformNode: transformNode,
+ genData: genData$1
+};
+
+/* */
+
+function transformNode$1 (el) {
+ var styleBinding = getBindingAttr(el, 'style', false /* getStatic */);
+ if (styleBinding) {
+ el.styleBinding = styleBinding;
+ }
+}
+
+function genData$2 (el) {
+ return el.styleBinding
+ ? ("style:(" + (el.styleBinding) + "),")
+ : ''
+}
+
+var style$1 = {
+ transformNode: transformNode$1,
+ genData: genData$2
+};
+
+var modules$1 = [
+ klass$1,
+ style$1
+];
+
+/* */
+
+var warn$3;
+
+function model$1 (
+ el,
+ dir,
+ _warn
+) {
+ warn$3 = _warn;
+ var value = dir.value;
+ var modifiers = dir.modifiers;
+ var tag = el.tag;
+ var type = el.attrsMap.type;
+ {
+ var dynamicType = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
+ if (tag === 'input' && dynamicType) {
+ warn$3(
+ "<input :type=\"" + dynamicType + "\" v-model=\"" + value + "\">:\n" +
+ "v-model does not support dynamic input types. Use v-if branches instead."
+ );
+ }
+ }
+ if (tag === 'select') {
+ genSelect(el, value);
+ } else if (tag === 'input' && type === 'checkbox') {
+ genCheckboxModel(el, value);
+ } else if (tag === 'input' && type === 'radio') {
+ genRadioModel(el, value);
+ } else {
+ genDefaultModel(el, value, modifiers);
+ }
+ // ensure runtime directive metadata
+ return true
+}
+
+function genCheckboxModel (el, value) {
+ if ("development" !== 'production' &&
+ el.attrsMap.checked != null) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
+ "inline checked attributes will be ignored when using v-model. " +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ var valueBinding = getBindingAttr(el, 'value') || 'null';
+ var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';
+ var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';
+ addProp(el, 'checked',
+ "Array.isArray(" + value + ")" +
+ "?_i(" + value + "," + valueBinding + ")>-1" +
+ ":_q(" + value + "," + trueValueBinding + ")"
+ );
+ addHandler(el, 'change',
+ "var $$a=" + value + "," +
+ '$$el=$event.target,' +
+ "$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");" +
+ 'if(Array.isArray($$a)){' +
+ "var $$v=" + valueBinding + "," +
+ '$$i=_i($$a,$$v);' +
+ "if($$c){$$i<0&&(" + value + "=$$a.concat($$v))}" +
+ "else{$$i>-1&&(" + value + "=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}" +
+ "}else{" + value + "=$$c}",
+ null, true
+ );
+}
+
+function genRadioModel (el, value) {
+ if ("development" !== 'production' &&
+ el.attrsMap.checked != null) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" checked>:\n" +
+ "inline checked attributes will be ignored when using v-model. " +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ var valueBinding = getBindingAttr(el, 'value') || 'null';
+ addProp(el, 'checked', ("_q(" + value + "," + valueBinding + ")"));
+ addHandler(el, 'change', (value + "=" + valueBinding), null, true);
+}
+
+function genDefaultModel (
+ el,
+ value,
+ modifiers
+) {
+ {
+ if (el.tag === 'input' && el.attrsMap.value) {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" value=\"" + (el.attrsMap.value) + "\">:\n" +
+ 'inline value attributes will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ if (el.tag === 'textarea' && el.children.length) {
+ warn$3(
+ "<textarea v-model=\"" + value + "\">:\n" +
+ 'inline content inside <textarea> will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ }
+ }
+
+ var type = el.attrsMap.type;
+ var ref = modifiers || {};
+ var lazy = ref.lazy;
+ var number = ref.number;
+ var trim = ref.trim;
+ var event = lazy || (isIE && type === 'range') ? 'change' : 'input';
+ var needCompositionGuard = !lazy && type !== 'range';
+ var isNative = el.tag === 'input' || el.tag === 'textarea';
+
+ var valueExpression = isNative
+ ? ("$event.target.value" + (trim ? '.trim()' : ''))
+ : "$event";
+ var code = number || type === 'number'
+ ? (value + "=_n(" + valueExpression + ")")
+ : (value + "=" + valueExpression);
+ if (isNative && needCompositionGuard) {
+ code = "if($event.target.composing)return;" + code;
+ }
+ // inputs with type="file" are read only and setting the input's
+ // value will throw an error.
+ if ("development" !== 'production' &&
+ type === 'file') {
+ warn$3(
+ "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
+ "File inputs are read only. Use a v-on:change listener instead."
+ );
+ }
+ addProp(el, 'value', isNative ? ("_s(" + value + ")") : ("(" + value + ")"));
+ addHandler(el, event, code, null, true);
+}
+
+function genSelect (el, value) {
+ {
+ el.children.some(checkOptionWarning);
+ }
+ var code = value + "=Array.prototype.filter" +
+ ".call($event.target.options,function(o){return o.selected})" +
+ ".map(function(o){return \"_value\" in o ? o._value : o.value})" +
+ (el.attrsMap.multiple == null ? '[0]' : '');
+ addHandler(el, 'change', code, null, true);
+}
+
+function checkOptionWarning (option) {
+ if (option.type === 1 &&
+ option.tag === 'option' &&
+ option.attrsMap.selected != null) {
+ warn$3(
+ "<select v-model=\"" + (option.parent.attrsMap['v-model']) + "\">:\n" +
+ 'inline selected attributes on <option> will be ignored when using v-model. ' +
+ 'Declare initial values in the component\'s data option instead.'
+ );
+ return true
+ }
+ return false
+}
+
+/* */
+
+function text (el, dir) {
+ if (dir.value) {
+ addProp(el, 'textContent', ("_s(" + (dir.value) + ")"));
+ }
+}
+
+/* */
+
+function html (el, dir) {
+ if (dir.value) {
+ addProp(el, 'innerHTML', ("_s(" + (dir.value) + ")"));
+ }
+}
+
+var directives$1 = {
+ model: model$1,
+ text: text,
+ html: html
+};
+
+/* */
+
+var cache = Object.create(null);
+
+var baseOptions = {
+ isIE: isIE,
+ expectHTML: true,
+ modules: modules$1,
+ staticKeys: genStaticKeys(modules$1),
+ directives: directives$1,
+ isReservedTag: isReservedTag,
+ isUnaryTag: isUnaryTag,
+ mustUseProp: mustUseProp,
+ getTagNamespace: getTagNamespace,
+ isPreTag: isPreTag
+};
+
+function compile$$1 (
+ template,
+ options
+) {
+ options = options
+ ? extend(extend({}, baseOptions), options)
+ : baseOptions;
+ return compile$1(template, options)
+}
+
+function compileToFunctions (
+ template,
+ options,
+ vm
+) {
+ var _warn = (options && options.warn) || warn;
+ // detect possible CSP restriction
+ /* istanbul ignore if */
+ {
+ try {
+ new Function('return 1');
+ } catch (e) {
+ if (e.toString().match(/unsafe-eval|CSP/)) {
+ _warn(
+ 'It seems you are using the standalone build of Vue.js in an ' +
+ 'environment with Content Security Policy that prohibits unsafe-eval. ' +
+ 'The template compiler cannot work in this environment. Consider ' +
+ 'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
+ 'templates into render functions.'
+ );
}
- return a === b ? 0 : a > b ? order : -order;
}
-
- // sort on a copy to avoid mutating original array
- return arr.slice().sort(comparator);
}
-
- /**
- * String contain helper
- *
- * @param {*} val
- * @param {String} search
- */
-
- function contains(val, search) {
- var i;
- if (isPlainObject(val)) {
- var keys = Object.keys(val);
- i = keys.length;
- while (i--) {
- if (contains(val[keys[i]], search)) {
- return true;
- }
- }
- } else if (isArray(val)) {
- i = val.length;
- while (i--) {
- if (contains(val[i], search)) {
- return true;
- }
- }
- } else if (val != null) {
- return val.toString().toLowerCase().indexOf(search) > -1;
+ var key = options && options.delimiters
+ ? String(options.delimiters) + template
+ : template;
+ if (cache[key]) {
+ return cache[key]
+ }
+ var res = {};
+ var compiled = compile$$1(template, options);
+ res.render = makeFunction(compiled.render);
+ var l = compiled.staticRenderFns.length;
+ res.staticRenderFns = new Array(l);
+ for (var i = 0; i < l; i++) {
+ res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i]);
+ }
+ {
+ if (res.render === noop || res.staticRenderFns.some(function (fn) { return fn === noop; })) {
+ _warn(
+ "failed to compile template:\n\n" + template + "\n\n" +
+ detectErrors(compiled.ast).join('\n') +
+ '\n\n',
+ vm
+ );
}
}
+ return (cache[key] = res)
+}
- var digitsRE = /(\d{3})(?=\d)/g;
-
- // asset collections must be a plain object.
- var filters = {
-
- orderBy: orderBy,
- filterBy: filterBy,
- limitBy: limitBy,
-
- /**
- * Stringify value.
- *
- * @param {Number} indent
- */
-
- json: {
- read: function read(value, indent) {
- return typeof value === 'string' ? value : JSON.stringify(value, null, arguments.length > 1 ? indent : 2);
- },
- write: function write(value) {
- try {
- return JSON.parse(value);
- } catch (e) {
- return value;
- }
- }
- },
-
- /**
- * 'abc' => 'Abc'
- */
-
- capitalize: function capitalize(value) {
- if (!value && value !== 0) return '';
- value = value.toString();
- return value.charAt(0).toUpperCase() + value.slice(1);
- },
-
- /**
- * 'abc' => 'ABC'
- */
+function makeFunction (code) {
+ try {
+ return new Function(code)
+ } catch (e) {
+ return noop
+ }
+}
- uppercase: function uppercase(value) {
- return value || value === 0 ? value.toString().toUpperCase() : '';
- },
+/* */
- /**
- * 'AbC' => 'abc'
- */
+var idToTemplate = cached(function (id) {
+ var el = query(id);
+ return el && el.innerHTML
+});
- lowercase: function lowercase(value) {
- return value || value === 0 ? value.toString().toLowerCase() : '';
- },
+var mount = Vue$3.prototype.$mount;
+Vue$3.prototype.$mount = function (
+ el,
+ hydrating
+) {
+ el = el && query(el);
- /**
- * 12345 => $12,345.00
- *
- * @param {String} sign
- * @param {Number} decimals Decimal places
- */
-
- currency: function currency(value, _currency, decimals) {
- value = parseFloat(value);
- if (!isFinite(value) || !value && value !== 0) return '';
- _currency = _currency != null ? _currency : '$';
- decimals = decimals != null ? decimals : 2;
- var stringified = Math.abs(value).toFixed(decimals);
- var _int = decimals ? stringified.slice(0, -1 - decimals) : stringified;
- var i = _int.length % 3;
- var head = i > 0 ? _int.slice(0, i) + (_int.length > 3 ? ',' : '') : '';
- var _float = decimals ? stringified.slice(-1 - decimals) : '';
- var sign = value < 0 ? '-' : '';
- return sign + _currency + head + _int.slice(i).replace(digitsRE, '$1,') + _float;
- },
+ /* istanbul ignore if */
+ if (el === document.body || el === document.documentElement) {
+ "development" !== 'production' && warn(
+ "Do not mount Vue to <html> or <body> - mount to normal elements instead."
+ );
+ return this
+ }
- /**
- * 'item' => 'items'
- *
- * @params
- * an array of strings corresponding to
- * the single, double, triple ... forms of the word to
- * be pluralized. When the number to be pluralized
- * exceeds the length of the args, it will use the last
- * entry in the array.
- *
- * e.g. ['single', 'double', 'triple', 'multiple']
- */
-
- pluralize: function pluralize(value) {
- var args = toArray(arguments, 1);
- var length = args.length;
- if (length > 1) {
- var index = value % 10 - 1;
- return index in args ? args[index] : args[length - 1];
+ var options = this.$options;
+ // resolve template/el and convert to render function
+ if (!options.render) {
+ var template = options.template;
+ if (template) {
+ if (typeof template === 'string') {
+ if (template.charAt(0) === '#') {
+ template = idToTemplate(template);
+ }
+ } else if (template.nodeType) {
+ template = template.innerHTML;
} else {
- return args[0] + (value === 1 ? '' : 's');
- }
- },
-
- /**
- * Debounce a handler function.
- *
- * @param {Function} handler
- * @param {Number} delay = 300
- * @return {Function}
- */
-
- debounce: function debounce(handler, delay) {
- if (!handler) return;
- if (!delay) {
- delay = 300;
- }
- return _debounce(handler, delay);
- }
- };
-
- function installGlobalAPI (Vue) {
- /**
- * Vue and every constructor that extends Vue has an
- * associated options object, which can be accessed during
- * compilation steps as `this.constructor.options`.
- *
- * These can be seen as the default options of every
- * Vue instance.
- */
-
- Vue.options = {
- directives: directives,
- elementDirectives: elementDirectives,
- filters: filters,
- transitions: {},
- components: {},
- partials: {},
- replace: true
- };
-
- /**
- * Expose useful internals
- */
-
- Vue.util = util;
- Vue.config = config;
- Vue.set = set;
- Vue['delete'] = del;
- Vue.nextTick = nextTick;
-
- /**
- * The following are exposed for advanced usage / plugins
- */
-
- Vue.compiler = compiler;
- Vue.FragmentFactory = FragmentFactory;
- Vue.internalDirectives = internalDirectives;
- Vue.parsers = {
- path: path,
- text: text,
- template: template,
- directive: directive,
- expression: expression
- };
-
- /**
- * Each instance constructor, including Vue, has a unique
- * cid. This enables us to create wrapped "child
- * constructors" for prototypal inheritance and cache them.
- */
-
- Vue.cid = 0;
- var cid = 1;
-
- /**
- * Class inheritance
- *
- * @param {Object} extendOptions
- */
-
- Vue.extend = function (extendOptions) {
- extendOptions = extendOptions || {};
- var Super = this;
- var isFirstExtend = Super.cid === 0;
- if (isFirstExtend && extendOptions._Ctor) {
- return extendOptions._Ctor;
- }
- var name = extendOptions.name || Super.options.name;
- if ('development' !== 'production') {
- if (!/^[a-zA-Z][\w-]*$/.test(name)) {
- warn('Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characaters and the hyphen.');
- name = null;
+ {
+ warn('invalid template option:' + template, this);
}
+ return this
}
- var Sub = createClass(name || 'VueComponent');
- Sub.prototype = Object.create(Super.prototype);
- Sub.prototype.constructor = Sub;
- Sub.cid = cid++;
- Sub.options = mergeOptions(Super.options, extendOptions);
- Sub['super'] = Super;
- // allow further extension
- Sub.extend = Super.extend;
- // create asset registers, so extended classes
- // can have their private assets too.
- config._assetTypes.forEach(function (type) {
- Sub[type] = Super[type];
- });
- // enable recursive self-lookup
- if (name) {
- Sub.options.components[name] = Sub;
- }
- // cache constructor
- if (isFirstExtend) {
- extendOptions._Ctor = Sub;
- }
- return Sub;
- };
-
- /**
- * A function that returns a sub-class constructor with the
- * given name. This gives us much nicer output when
- * logging instances in the console.
- *
- * @param {String} name
- * @return {Function}
- */
-
- function createClass(name) {
- /* eslint-disable no-new-func */
- return new Function('return function ' + classify(name) + ' (options) { this._init(options) }')();
- /* eslint-enable no-new-func */
+ } else if (el) {
+ template = getOuterHTML(el);
+ }
+ if (template) {
+ var ref = compileToFunctions(template, {
+ warn: warn,
+ shouldDecodeNewlines: shouldDecodeNewlines,
+ delimiters: options.delimiters
+ }, this);
+ var render = ref.render;
+ var staticRenderFns = ref.staticRenderFns;
+ options.render = render;
+ options.staticRenderFns = staticRenderFns;
}
-
- /**
- * Plugin system
- *
- * @param {Object} plugin
- */
-
- Vue.use = function (plugin) {
- /* istanbul ignore if */
- if (plugin.installed) {
- return;
- }
- // additional parameters
- var args = toArray(arguments, 1);
- args.unshift(this);
- if (typeof plugin.install === 'function') {
- plugin.install.apply(plugin, args);
- } else {
- plugin.apply(null, args);
- }
- plugin.installed = true;
- return this;
- };
-
- /**
- * Apply a global mixin by merging it into the default
- * options.
- */
-
- Vue.mixin = function (mixin) {
- Vue.options = mergeOptions(Vue.options, mixin);
- };
-
- /**
- * Create asset registration methods with the following
- * signature:
- *
- * @param {String} id
- * @param {*} definition
- */
-
- config._assetTypes.forEach(function (type) {
- Vue[type] = function (id, definition) {
- if (!definition) {
- return this.options[type + 's'][id];
- } else {
- /* istanbul ignore if */
- if ('development' !== 'production') {
- if (type === 'component' && (commonTagRE.test(id) || reservedTagRE.test(id))) {
- warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + id);
- }
- }
- if (type === 'component' && isPlainObject(definition)) {
- if (!definition.name) {
- definition.name = id;
- }
- definition = Vue.extend(definition);
- }
- this.options[type + 's'][id] = definition;
- return definition;
- }
- };
- });
-
- // expose internal transition API
- extend(Vue.transition, transition);
}
+ return mount.call(this, el, hydrating)
+};
- installGlobalAPI(Vue);
-
- Vue.version = '1.0.26';
+/**
+ * Get outerHTML of elements, taking care
+ * of SVG elements in IE as well.
+ */
+function getOuterHTML (el) {
+ if (el.outerHTML) {
+ return el.outerHTML
+ } else {
+ var container = document.createElement('div');
+ container.appendChild(el.cloneNode(true));
+ return container.innerHTML
+ }
+}
- // devtools global hook
- /* istanbul ignore next */
- setTimeout(function () {
- if (config.devtools) {
- if (devtools) {
- devtools.emit('init', Vue);
- } else if ('development' !== 'production' && inBrowser && /Chrome\/\d+/.test(window.navigator.userAgent)) {
- console.log('Download the Vue Devtools for a better development experience:\n' + 'https://github.com/vuejs/vue-devtools');
- }
- }
- }, 0);
+Vue$3.compile = compileToFunctions;
- return Vue;
+return Vue$3;
-})); \ No newline at end of file
+})));
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
index 2c9a8a0e117..f86786dd454 100644
--- a/vendor/assets/javascripts/vue.min.js
+++ b/vendor/assets/javascripts/vue.min.js
@@ -1,9 +1,7 @@
/*!
- * Vue.js v1.0.26
- * (c) 2016 Evan You
+ * Vue.js v2.0.3
+ * (c) 2014-2016 Evan You
* Released under the MIT License.
*/
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vue=e()}(this,function(){"use strict";function t(e,n,r){if(i(e,n))return void(e[n]=r);if(e._isVue)return void t(e._data,n,r);var s=e.__ob__;if(!s)return void(e[n]=r);if(s.convert(n,r),s.dep.notify(),s.vms)for(var o=s.vms.length;o--;){var a=s.vms[o];a._proxy(n),a._digest()}return r}function e(t,e){if(i(t,e)){delete t[e];var n=t.__ob__;if(!n)return void(t._isVue&&(delete t._data[e],t._digest()));if(n.dep.notify(),n.vms)for(var r=n.vms.length;r--;){var s=n.vms[r];s._unproxy(e),s._digest()}}}function i(t,e){return Oi.call(t,e)}function n(t){return Ti.test(t)}function r(t){var e=(t+"").charCodeAt(0);return 36===e||95===e}function s(t){return null==t?"":t.toString()}function o(t){if("string"!=typeof t)return t;var e=Number(t);return isNaN(e)?t:e}function a(t){return"true"===t?!0:"false"===t?!1:t}function h(t){var e=t.charCodeAt(0),i=t.charCodeAt(t.length-1);return e!==i||34!==e&&39!==e?t:t.slice(1,-1)}function l(t){return t.replace(Ni,c)}function c(t,e){return e?e.toUpperCase():""}function u(t){return t.replace(ji,"$1-$2").toLowerCase()}function f(t){return t.replace(Ei,c)}function p(t,e){return function(i){var n=arguments.length;return n?n>1?t.apply(e,arguments):t.call(e,i):t.call(e)}}function d(t,e){e=e||0;for(var i=t.length-e,n=new Array(i);i--;)n[i]=t[i+e];return n}function v(t,e){for(var i=Object.keys(e),n=i.length;n--;)t[i[n]]=e[i[n]];return t}function m(t){return null!==t&&"object"==typeof t}function g(t){return Si.call(t)===Fi}function _(t,e,i,n){Object.defineProperty(t,e,{value:i,enumerable:!!n,writable:!0,configurable:!0})}function y(t,e){var i,n,r,s,o,a=function h(){var a=Date.now()-s;e>a&&a>=0?i=setTimeout(h,e-a):(i=null,o=t.apply(r,n),i||(r=n=null))};return function(){return r=this,n=arguments,s=Date.now(),i||(i=setTimeout(a,e)),o}}function b(t,e){for(var i=t.length;i--;)if(t[i]===e)return i;return-1}function w(t){var e=function i(){return i.cancelled?void 0:t.apply(this,arguments)};return e.cancel=function(){e.cancelled=!0},e}function C(t,e){return t==e||(m(t)&&m(e)?JSON.stringify(t)===JSON.stringify(e):!1)}function $(t){this.size=0,this.limit=t,this.head=this.tail=void 0,this._keymap=Object.create(null)}function k(){var t,e=en.slice(hn,on).trim();if(e){t={};var i=e.match(vn);t.name=i[0],i.length>1&&(t.args=i.slice(1).map(x))}t&&(nn.filters=nn.filters||[]).push(t),hn=on+1}function x(t){if(mn.test(t))return{value:o(t),dynamic:!1};var e=h(t),i=e===t;return{value:i?t:e,dynamic:i}}function A(t){var e=dn.get(t);if(e)return e;for(en=t,ln=cn=!1,un=fn=pn=0,hn=0,nn={},on=0,an=en.length;an>on;on++)if(sn=rn,rn=en.charCodeAt(on),ln)39===rn&&92!==sn&&(ln=!ln);else if(cn)34===rn&&92!==sn&&(cn=!cn);else if(124===rn&&124!==en.charCodeAt(on+1)&&124!==en.charCodeAt(on-1))null==nn.expression?(hn=on+1,nn.expression=en.slice(0,on).trim()):k();else switch(rn){case 34:cn=!0;break;case 39:ln=!0;break;case 40:pn++;break;case 41:pn--;break;case 91:fn++;break;case 93:fn--;break;case 123:un++;break;case 125:un--}return null==nn.expression?nn.expression=en.slice(0,on).trim():0!==hn&&k(),dn.put(t,nn),nn}function O(t){return t.replace(_n,"\\$&")}function T(){var t=O(An.delimiters[0]),e=O(An.delimiters[1]),i=O(An.unsafeDelimiters[0]),n=O(An.unsafeDelimiters[1]);bn=new RegExp(i+"((?:.|\\n)+?)"+n+"|"+t+"((?:.|\\n)+?)"+e,"g"),wn=new RegExp("^"+i+"((?:.|\\n)+?)"+n+"$"),yn=new $(1e3)}function N(t){yn||T();var e=yn.get(t);if(e)return e;if(!bn.test(t))return null;for(var i,n,r,s,o,a,h=[],l=bn.lastIndex=0;i=bn.exec(t);)n=i.index,n>l&&h.push({value:t.slice(l,n)}),r=wn.test(i[0]),s=r?i[1]:i[2],o=s.charCodeAt(0),a=42===o,s=a?s.slice(1):s,h.push({tag:!0,value:s.trim(),html:r,oneTime:a}),l=n+i[0].length;return l<t.length&&h.push({value:t.slice(l)}),yn.put(t,h),h}function j(t,e){return t.length>1?t.map(function(t){return E(t,e)}).join("+"):E(t[0],e,!0)}function E(t,e,i){return t.tag?t.oneTime&&e?'"'+e.$eval(t.value)+'"':S(t.value,i):'"'+t.value+'"'}function S(t,e){if(Cn.test(t)){var i=A(t);return i.filters?"this._applyFilters("+i.expression+",null,"+JSON.stringify(i.filters)+",false)":"("+t+")"}return e?t:"("+t+")"}function F(t,e,i,n){R(t,1,function(){e.appendChild(t)},i,n)}function D(t,e,i,n){R(t,1,function(){B(t,e)},i,n)}function P(t,e,i){R(t,-1,function(){z(t)},e,i)}function R(t,e,i,n,r){var s=t.__v_trans;if(!s||!s.hooks&&!qi||!n._isCompiled||n.$parent&&!n.$parent._isCompiled)return i(),void(r&&r());var o=e>0?"enter":"leave";s[o](i,r)}function L(t){if("string"==typeof t){t=document.querySelector(t)}return t}function H(t){if(!t)return!1;var e=t.ownerDocument.documentElement,i=t.parentNode;return e===t||e===i||!(!i||1!==i.nodeType||!e.contains(i))}function I(t,e){var i=t.getAttribute(e);return null!==i&&t.removeAttribute(e),i}function M(t,e){var i=I(t,":"+e);return null===i&&(i=I(t,"v-bind:"+e)),i}function V(t,e){return t.hasAttribute(e)||t.hasAttribute(":"+e)||t.hasAttribute("v-bind:"+e)}function B(t,e){e.parentNode.insertBefore(t,e)}function W(t,e){e.nextSibling?B(t,e.nextSibling):e.parentNode.appendChild(t)}function z(t){t.parentNode.removeChild(t)}function U(t,e){e.firstChild?B(t,e.firstChild):e.appendChild(t)}function J(t,e){var i=t.parentNode;i&&i.replaceChild(e,t)}function q(t,e,i,n){t.addEventListener(e,i,n)}function Q(t,e,i){t.removeEventListener(e,i)}function G(t){var e=t.className;return"object"==typeof e&&(e=e.baseVal||""),e}function Z(t,e){Mi&&!/svg$/.test(t.namespaceURI)?t.className=e:t.setAttribute("class",e)}function X(t,e){if(t.classList)t.classList.add(e);else{var i=" "+G(t)+" ";i.indexOf(" "+e+" ")<0&&Z(t,(i+e).trim())}}function Y(t,e){if(t.classList)t.classList.remove(e);else{for(var i=" "+G(t)+" ",n=" "+e+" ";i.indexOf(n)>=0;)i=i.replace(n," ");Z(t,i.trim())}t.className||t.removeAttribute("class")}function K(t,e){var i,n;if(it(t)&&at(t.content)&&(t=t.content),t.hasChildNodes())for(tt(t),n=e?document.createDocumentFragment():document.createElement("div");i=t.firstChild;)n.appendChild(i);return n}function tt(t){for(var e;e=t.firstChild,et(e);)t.removeChild(e);for(;e=t.lastChild,et(e);)t.removeChild(e)}function et(t){return t&&(3===t.nodeType&&!t.data.trim()||8===t.nodeType)}function it(t){return t.tagName&&"template"===t.tagName.toLowerCase()}function nt(t,e){var i=An.debug?document.createComment(t):document.createTextNode(e?" ":"");return i.__v_anchor=!0,i}function rt(t){if(t.hasAttributes())for(var e=t.attributes,i=0,n=e.length;n>i;i++){var r=e[i].name;if(Nn.test(r))return l(r.replace(Nn,""))}}function st(t,e,i){for(var n;t!==e;)n=t.nextSibling,i(t),t=n;i(e)}function ot(t,e,i,n,r){function s(){if(a++,o&&a>=h.length){for(var t=0;t<h.length;t++)n.appendChild(h[t]);r&&r()}}var o=!1,a=0,h=[];st(t,e,function(t){t===e&&(o=!0),h.push(t),P(t,i,s)})}function at(t){return t&&11===t.nodeType}function ht(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}function lt(t,e){var i=t.tagName.toLowerCase(),n=t.hasAttributes();if(jn.test(i)||En.test(i)){if(n)return ct(t,e)}else{if(gt(e,"components",i))return{id:i};var r=n&&ct(t,e);if(r)return r}}function ct(t,e){var i=t.getAttribute("is");if(null!=i){if(gt(e,"components",i))return t.removeAttribute("is"),{id:i}}else if(i=M(t,"is"),null!=i)return{id:i,dynamic:!0}}function ut(e,n){var r,s,o;for(r in n)s=e[r],o=n[r],i(e,r)?m(s)&&m(o)&&ut(s,o):t(e,r,o);return e}function ft(t,e){var i=Object.create(t||null);return e?v(i,vt(e)):i}function pt(t){if(t.components)for(var e,i=t.components=vt(t.components),n=Object.keys(i),r=0,s=n.length;s>r;r++){var o=n[r];jn.test(o)||En.test(o)||(e=i[o],g(e)&&(i[o]=wi.extend(e)))}}function dt(t){var e,i,n=t.props;if(Di(n))for(t.props={},e=n.length;e--;)i=n[e],"string"==typeof i?t.props[i]=null:i.name&&(t.props[i.name]=i);else if(g(n)){var r=Object.keys(n);for(e=r.length;e--;)i=n[r[e]],"function"==typeof i&&(n[r[e]]={type:i})}}function vt(t){if(Di(t)){for(var e,i={},n=t.length;n--;){e=t[n];var r="function"==typeof e?e.options&&e.options.name||e.id:e.name||e.id;r&&(i[r]=e)}return i}return t}function mt(t,e,n){function r(i){var r=Sn[i]||Fn;o[i]=r(t[i],e[i],n,i)}pt(e),dt(e);var s,o={};if(e["extends"]&&(t="function"==typeof e["extends"]?mt(t,e["extends"].options,n):mt(t,e["extends"],n)),e.mixins)for(var a=0,h=e.mixins.length;h>a;a++){var l=e.mixins[a],c=l.prototype instanceof wi?l.options:l;t=mt(t,c,n)}for(s in t)r(s);for(s in e)i(t,s)||r(s);return o}function gt(t,e,i,n){if("string"==typeof i){var r,s=t[e],o=s[i]||s[r=l(i)]||s[r.charAt(0).toUpperCase()+r.slice(1)];return o}}function _t(){this.id=Dn++,this.subs=[]}function yt(t){Hn=!1,t(),Hn=!0}function bt(t){if(this.value=t,this.dep=new _t,_(t,"__ob__",this),Di(t)){var e=Pi?wt:Ct;e(t,Rn,Ln),this.observeArray(t)}else this.walk(t)}function wt(t,e){t.__proto__=e}function Ct(t,e,i){for(var n=0,r=i.length;r>n;n++){var s=i[n];_(t,s,e[s])}}function $t(t,e){if(t&&"object"==typeof t){var n;return i(t,"__ob__")&&t.__ob__ instanceof bt?n=t.__ob__:Hn&&(Di(t)||g(t))&&Object.isExtensible(t)&&!t._isVue&&(n=new bt(t)),n&&e&&n.addVm(e),n}}function kt(t,e,i){var n=new _t,r=Object.getOwnPropertyDescriptor(t,e);if(!r||r.configurable!==!1){var s=r&&r.get,o=r&&r.set,a=$t(i);Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){var e=s?s.call(t):i;if(_t.target&&(n.depend(),a&&a.dep.depend(),Di(e)))for(var r,o=0,h=e.length;h>o;o++)r=e[o],r&&r.__ob__&&r.__ob__.dep.depend();return e},set:function(e){var r=s?s.call(t):i;e!==r&&(o?o.call(t,e):i=e,a=$t(e),n.notify())}})}}function xt(t){t.prototype._init=function(t){t=t||{},this.$el=null,this.$parent=t.parent,this.$root=this.$parent?this.$parent.$root:this,this.$children=[],this.$refs={},this.$els={},this._watchers=[],this._directives=[],this._uid=Mn++,this._isVue=!0,this._events={},this._eventsCount={},this._isFragment=!1,this._fragment=this._fragmentStart=this._fragmentEnd=null,this._isCompiled=this._isDestroyed=this._isReady=this._isAttached=this._isBeingDestroyed=this._vForRemoving=!1,this._unlinkFn=null,this._context=t._context||this.$parent,this._scope=t._scope,this._frag=t._frag,this._frag&&this._frag.children.push(this),this.$parent&&this.$parent.$children.push(this),t=this.$options=mt(this.constructor.options,t,this),this._updateRef(),this._data={},this._callHook("init"),this._initState(),this._initEvents(),this._callHook("created"),t.el&&this.$mount(t.el)}}function At(t){if(void 0===t)return"eof";var e=t.charCodeAt(0);switch(e){case 91:case 93:case 46:case 34:case 39:case 48:return t;case 95:case 36:return"ident";case 32:case 9:case 10:case 13:case 160:case 65279:case 8232:case 8233:return"ws"}return e>=97&&122>=e||e>=65&&90>=e?"ident":e>=49&&57>=e?"number":"else"}function Ot(t){var e=t.trim();return"0"===t.charAt(0)&&isNaN(t)?!1:n(e)?h(e):"*"+e}function Tt(t){function e(){var e=t[c+1];return u===Xn&&"'"===e||u===Yn&&'"'===e?(c++,n="\\"+e,p[Bn](),!0):void 0}var i,n,r,s,o,a,h,l=[],c=-1,u=Jn,f=0,p=[];for(p[Wn]=function(){void 0!==r&&(l.push(r),r=void 0)},p[Bn]=function(){void 0===r?r=n:r+=n},p[zn]=function(){p[Bn](),f++},p[Un]=function(){if(f>0)f--,u=Zn,p[Bn]();else{if(f=0,r=Ot(r),r===!1)return!1;p[Wn]()}};null!=u;)if(c++,i=t[c],"\\"!==i||!e()){if(s=At(i),h=er[u],o=h[s]||h["else"]||tr,o===tr)return;if(u=o[0],a=p[o[1]],a&&(n=o[2],n=void 0===n?i:n,a()===!1))return;if(u===Kn)return l.raw=t,l}}function Nt(t){var e=Vn.get(t);return e||(e=Tt(t),e&&Vn.put(t,e)),e}function jt(t,e){return It(e).get(t)}function Et(e,i,n){var r=e;if("string"==typeof i&&(i=Tt(i)),!i||!m(e))return!1;for(var s,o,a=0,h=i.length;h>a;a++)s=e,o=i[a],"*"===o.charAt(0)&&(o=It(o.slice(1)).get.call(r,r)),h-1>a?(e=e[o],m(e)||(e={},t(s,o,e))):Di(e)?e.$set(o,n):o in e?e[o]=n:t(e,o,n);return!0}function St(){}function Ft(t,e){var i=vr.length;return vr[i]=e?t.replace(lr,"\\n"):t,'"'+i+'"'}function Dt(t){var e=t.charAt(0),i=t.slice(1);return sr.test(i)?t:(i=i.indexOf('"')>-1?i.replace(ur,Pt):i,e+"scope."+i)}function Pt(t,e){return vr[e]}function Rt(t){ar.test(t),vr.length=0;var e=t.replace(cr,Ft).replace(hr,"");return e=(" "+e).replace(pr,Dt).replace(ur,Pt),Lt(e)}function Lt(t){try{return new Function("scope","return "+t+";")}catch(e){return St}}function Ht(t){var e=Nt(t);return e?function(t,i){Et(t,e,i)}:void 0}function It(t,e){t=t.trim();var i=nr.get(t);if(i)return e&&!i.set&&(i.set=Ht(i.exp)),i;var n={exp:t};return n.get=Mt(t)&&t.indexOf("[")<0?Lt("scope."+t):Rt(t),e&&(n.set=Ht(t)),nr.put(t,n),n}function Mt(t){return fr.test(t)&&!dr.test(t)&&"Math."!==t.slice(0,5)}function Vt(){gr.length=0,_r.length=0,yr={},br={},wr=!1}function Bt(){for(var t=!0;t;)t=!1,Wt(gr),Wt(_r),gr.length?t=!0:(Li&&An.devtools&&Li.emit("flush"),Vt())}function Wt(t){for(var e=0;e<t.length;e++){var i=t[e],n=i.id;yr[n]=null,i.run()}t.length=0}function zt(t){var e=t.id;if(null==yr[e]){var i=t.user?_r:gr;yr[e]=i.length,i.push(t),wr||(wr=!0,Yi(Bt))}}function Ut(t,e,i,n){n&&v(this,n);var r="function"==typeof e;if(this.vm=t,t._watchers.push(this),this.expression=e,this.cb=i,this.id=++Cr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new Ki,this.newDepIds=new Ki,this.prevError=null,r)this.getter=e,this.setter=void 0;else{var s=It(e,this.twoWay);this.getter=s.get,this.setter=s.set}this.value=this.lazy?void 0:this.get(),this.queued=this.shallow=!1}function Jt(t,e){var i=void 0,n=void 0;e||(e=$r,e.clear());var r=Di(t),s=m(t);if((r||s)&&Object.isExtensible(t)){if(t.__ob__){var o=t.__ob__.dep.id;if(e.has(o))return;e.add(o)}if(r)for(i=t.length;i--;)Jt(t[i],e);else if(s)for(n=Object.keys(t),i=n.length;i--;)Jt(t[n[i]],e)}}function qt(t){return it(t)&&at(t.content)}function Qt(t,e){var i=e?t:t.trim(),n=xr.get(i);if(n)return n;var r=document.createDocumentFragment(),s=t.match(Tr),o=Nr.test(t),a=jr.test(t);if(s||o||a){var h=s&&s[1],l=Or[h]||Or.efault,c=l[0],u=l[1],f=l[2],p=document.createElement("div");for(p.innerHTML=u+t+f;c--;)p=p.lastChild;for(var d;d=p.firstChild;)r.appendChild(d)}else r.appendChild(document.createTextNode(t));return e||tt(r),xr.put(i,r),r}function Gt(t){if(qt(t))return Qt(t.innerHTML);if("SCRIPT"===t.tagName)return Qt(t.textContent);for(var e,i=Zt(t),n=document.createDocumentFragment();e=i.firstChild;)n.appendChild(e);return tt(n),n}function Zt(t){if(!t.querySelectorAll)return t.cloneNode();var e,i,n,r=t.cloneNode(!0);if(Er){var s=r;if(qt(t)&&(t=t.content,s=r.content),i=t.querySelectorAll("template"),i.length)for(n=s.querySelectorAll("template"),e=n.length;e--;)n[e].parentNode.replaceChild(Zt(i[e]),n[e])}if(Sr)if("TEXTAREA"===t.tagName)r.value=t.value;else if(i=t.querySelectorAll("textarea"),i.length)for(n=r.querySelectorAll("textarea"),e=n.length;e--;)n[e].value=i[e].value;return r}function Xt(t,e,i){var n,r;return at(t)?(tt(t),e?Zt(t):t):("string"==typeof t?i||"#"!==t.charAt(0)?r=Qt(t,i):(r=Ar.get(t),r||(n=document.getElementById(t.slice(1)),n&&(r=Gt(n),Ar.put(t,r)))):t.nodeType&&(r=Gt(t)),r&&e?Zt(r):r)}function Yt(t,e,i,n,r,s){this.children=[],this.childFrags=[],this.vm=e,this.scope=r,this.inserted=!1,this.parentFrag=s,s&&s.childFrags.push(this),this.unlink=t(e,i,n,r,this);var o=this.single=1===i.childNodes.length&&!i.childNodes[0].__v_anchor;o?(this.node=i.childNodes[0],this.before=Kt,this.remove=te):(this.node=nt("fragment-start"),this.end=nt("fragment-end"),this.frag=i,U(this.node,i),i.appendChild(this.end),this.before=ee,this.remove=ie),this.node.__v_frag=this}function Kt(t,e){this.inserted=!0;var i=e!==!1?D:B;i(this.node,t,this.vm),H(this.node)&&this.callHook(ne)}function te(){this.inserted=!1;var t=H(this.node),e=this;this.beforeRemove(),P(this.node,this.vm,function(){t&&e.callHook(re),e.destroy()})}function ee(t,e){this.inserted=!0;var i=this.vm,n=e!==!1?D:B;st(this.node,this.end,function(e){n(e,t,i)}),H(this.node)&&this.callHook(ne)}function ie(){this.inserted=!1;var t=this,e=H(this.node);this.beforeRemove(),ot(this.node,this.end,this.vm,this.frag,function(){e&&t.callHook(re),t.destroy()})}function ne(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function re(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}function se(t,e){this.vm=t;var i,n="string"==typeof e;n||it(e)&&!e.hasAttribute("v-if")?i=Xt(e,!0):(i=document.createDocumentFragment(),i.appendChild(e)),this.template=i;var r,s=t.constructor.cid;if(s>0){var o=s+(n?e:ht(e));r=Pr.get(o),r||(r=De(i,t.$options,!0),Pr.put(o,r))}else r=De(i,t.$options,!0);this.linker=r}function oe(t,e,i){var n=t.node.previousSibling;if(n){for(t=n.__v_frag;!(t&&t.forId===i&&t.inserted||n===e);){if(n=n.previousSibling,!n)return;t=n.__v_frag}return t}}function ae(t){var e=t.node;if(t.end)for(;!e.__vue__&&e!==t.end&&e.nextSibling;)e=e.nextSibling;return e.__vue__}function he(t){for(var e=-1,i=new Array(Math.floor(t));++e<t;)i[e]=e;return i}function le(t,e,i,n){return n?"$index"===n?t:n.charAt(0).match(/\w/)?jt(i,n):i[n]:e||i}function ce(t,e,i){for(var n,r,s,o=e?[]:null,a=0,h=t.options.length;h>a;a++)if(n=t.options[a],s=i?n.hasAttribute("selected"):n.selected){if(r=n.hasOwnProperty("_value")?n._value:n.value,!e)return r;o.push(r)}return o}function ue(t,e){for(var i=t.length;i--;)if(C(t[i],e))return i;return-1}function fe(t,e){var i=e.map(function(t){var e=t.charCodeAt(0);return e>47&&58>e?parseInt(t,10):1===t.length&&(e=t.toUpperCase().charCodeAt(0),e>64&&91>e)?e:is[t]});return i=[].concat.apply([],i),function(e){return i.indexOf(e.keyCode)>-1?t.call(this,e):void 0}}function pe(t){return function(e){return e.stopPropagation(),t.call(this,e)}}function de(t){return function(e){return e.preventDefault(),t.call(this,e)}}function ve(t){return function(e){return e.target===e.currentTarget?t.call(this,e):void 0}}function me(t){if(as[t])return as[t];var e=ge(t);return as[t]=as[e]=e,e}function ge(t){t=u(t);var e=l(t),i=e.charAt(0).toUpperCase()+e.slice(1);hs||(hs=document.createElement("div"));var n,r=rs.length;if("filter"!==e&&e in hs.style)return{kebab:t,camel:e};for(;r--;)if(n=ss[r]+i,n in hs.style)return{kebab:rs[r]+t,camel:n}}function _e(t){var e=[];if(Di(t))for(var i=0,n=t.length;n>i;i++){var r=t[i];if(r)if("string"==typeof r)e.push(r);else for(var s in r)r[s]&&e.push(s)}else if(m(t))for(var o in t)t[o]&&e.push(o);return e}function ye(t,e,i){if(e=e.trim(),-1===e.indexOf(" "))return void i(t,e);for(var n=e.split(/\s+/),r=0,s=n.length;s>r;r++)i(t,n[r])}function be(t,e,i){function n(){++s>=r?i():t[s].call(e,n)}var r=t.length,s=0;t[0].call(e,n)}function we(t,e,i){for(var r,s,o,a,h,c,f,p=[],d=Object.keys(e),v=d.length;v--;)s=d[v],r=e[s]||ks,h=l(s),xs.test(h)&&(f={name:s,path:h,options:r,mode:$s.ONE_WAY,raw:null},o=u(s),null===(a=M(t,o))&&(null!==(a=M(t,o+".sync"))?f.mode=$s.TWO_WAY:null!==(a=M(t,o+".once"))&&(f.mode=$s.ONE_TIME)),null!==a?(f.raw=a,c=A(a),a=c.expression,f.filters=c.filters,n(a)&&!c.filters?f.optimizedLiteral=!0:f.dynamic=!0,f.parentPath=a):null!==(a=I(t,o))&&(f.raw=a),p.push(f));return Ce(p)}function Ce(t){return function(e,n){e._props={};for(var r,s,l,c,f,p=e.$options.propsData,d=t.length;d--;)if(r=t[d],f=r.raw,s=r.path,l=r.options,e._props[s]=r,p&&i(p,s)&&ke(e,r,p[s]),null===f)ke(e,r,void 0);else if(r.dynamic)r.mode===$s.ONE_TIME?(c=(n||e._context||e).$get(r.parentPath),ke(e,r,c)):e._context?e._bindDir({name:"prop",def:Os,prop:r},null,null,n):ke(e,r,e.$get(r.parentPath));else if(r.optimizedLiteral){var v=h(f);c=v===f?a(o(f)):v,ke(e,r,c)}else c=l.type!==Boolean||""!==f&&f!==u(r.name)?f:!0,ke(e,r,c)}}function $e(t,e,i,n){var r=e.dynamic&&Mt(e.parentPath),s=i;void 0===s&&(s=Ae(t,e)),s=Te(e,s,t);var o=s!==i;Oe(e,s,t)||(s=void 0),r&&!o?yt(function(){n(s)}):n(s)}function ke(t,e,i){$e(t,e,i,function(i){kt(t,e.path,i)})}function xe(t,e,i){$e(t,e,i,function(i){t[e.path]=i})}function Ae(t,e){var n=e.options;if(!i(n,"default"))return n.type===Boolean?!1:void 0;var r=n["default"];return m(r),"function"==typeof r&&n.type!==Function?r.call(t):r}function Oe(t,e,i){if(!t.options.required&&(null===t.raw||null==e))return!0;var n=t.options,r=n.type,s=!r,o=[];if(r){Di(r)||(r=[r]);for(var a=0;a<r.length&&!s;a++){var h=Ne(e,r[a]);o.push(h.expectedType),s=h.valid}}if(!s)return!1;var l=n.validator;return!l||l(e)}function Te(t,e,i){var n=t.options.coerce;return n&&"function"==typeof n?n(e):e}function Ne(t,e){var i,n;return e===String?(n="string",i=typeof t===n):e===Number?(n="number",i=typeof t===n):e===Boolean?(n="boolean",i=typeof t===n):e===Function?(n="function",i=typeof t===n):e===Object?(n="object",i=g(t)):e===Array?(n="array",i=Di(t)):i=t instanceof e,{valid:i,expectedType:n}}function je(t){Ts.push(t),Ns||(Ns=!0,Yi(Ee))}function Ee(){for(var t=document.documentElement.offsetHeight,e=0;e<Ts.length;e++)Ts[e]();return Ts=[],Ns=!1,t}function Se(t,e,i,n){this.id=e,this.el=t,this.enterClass=i&&i.enterClass||e+"-enter",this.leaveClass=i&&i.leaveClass||e+"-leave",this.hooks=i,this.vm=n,this.pendingCssEvent=this.pendingCssCb=this.cancel=this.pendingJsCb=this.op=this.cb=null,this.justEntered=!1,this.entered=this.left=!1,this.typeCache={},this.type=i&&i.type;var r=this;["enterNextTick","enterDone","leaveNextTick","leaveDone"].forEach(function(t){r[t]=p(r[t],r)})}function Fe(t){if(/svg$/.test(t.namespaceURI)){var e=t.getBoundingClientRect();return!(e.width||e.height)}return!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function De(t,e,i){var n=i||!e._asComponent?Ve(t,e):null,r=n&&n.terminal||ri(t)||!t.hasChildNodes()?null:qe(t.childNodes,e);return function(t,e,i,s,o){var a=d(e.childNodes),h=Pe(function(){n&&n(t,e,i,s,o),r&&r(t,a,i,s,o)},t);return Le(t,h)}}function Pe(t,e){e._directives=[];var i=e._directives.length;t();var n=e._directives.slice(i);n.sort(Re);for(var r=0,s=n.length;s>r;r++)n[r]._bind();return n}function Re(t,e){return t=t.descriptor.def.priority||zs,e=e.descriptor.def.priority||zs,t>e?-1:t===e?0:1}function Le(t,e,i,n){function r(r){He(t,e,r),i&&n&&He(i,n)}return r.dirs=e,r}function He(t,e,i){for(var n=e.length;n--;)e[n]._teardown()}function Ie(t,e,i,n){var r=we(e,i,t),s=Pe(function(){r(t,n)},t);return Le(t,s)}function Me(t,e,i){var n,r,s=e._containerAttrs,o=e._replacerAttrs;return 11!==t.nodeType&&(e._asComponent?(s&&i&&(n=ti(s,i)),o&&(r=ti(o,e))):r=ti(t.attributes,e)),e._containerAttrs=e._replacerAttrs=null,function(t,e,i){var s,o=t._context;o&&n&&(s=Pe(function(){n(o,e,null,i)},o));var a=Pe(function(){r&&r(t,e)},t);return Le(t,a,o,s)}}function Ve(t,e){var i=t.nodeType;return 1!==i||ri(t)?3===i&&t.data.trim()?We(t,e):null:Be(t,e)}function Be(t,e){if("TEXTAREA"===t.tagName){var i=N(t.value);i&&(t.setAttribute(":value",j(i)),t.value="")}var n,r=t.hasAttributes(),s=r&&d(t.attributes);return r&&(n=Xe(t,s,e)),n||(n=Ge(t,e)),n||(n=Ze(t,e)),!n&&r&&(n=ti(s,e)),n}function We(t,e){if(t._skip)return ze;var i=N(t.wholeText);if(!i)return null;for(var n=t.nextSibling;n&&3===n.nodeType;)n._skip=!0,n=n.nextSibling;for(var r,s,o=document.createDocumentFragment(),a=0,h=i.length;h>a;a++)s=i[a],r=s.tag?Ue(s,e):document.createTextNode(s.value),o.appendChild(r);return Je(i,o,e)}function ze(t,e){z(e)}function Ue(t,e){function i(e){if(!t.descriptor){var i=A(t.value);t.descriptor={name:e,def:bs[e],expression:i.expression,filters:i.filters}}}var n;return t.oneTime?n=document.createTextNode(t.value):t.html?(n=document.createComment("v-html"),i("html")):(n=document.createTextNode(" "),i("text")),n}function Je(t,e){return function(i,n,r,o){for(var a,h,l,c=e.cloneNode(!0),u=d(c.childNodes),f=0,p=t.length;p>f;f++)a=t[f],h=a.value,a.tag&&(l=u[f],a.oneTime?(h=(o||i).$eval(h),a.html?J(l,Xt(h,!0)):l.data=s(h)):i._bindDir(a.descriptor,l,r,o));J(n,c)}}function qe(t,e){for(var i,n,r,s=[],o=0,a=t.length;a>o;o++)r=t[o],i=Ve(r,e),n=i&&i.terminal||"SCRIPT"===r.tagName||!r.hasChildNodes()?null:qe(r.childNodes,e),s.push(i,n);return s.length?Qe(s):null}function Qe(t){return function(e,i,n,r,s){for(var o,a,h,l=0,c=0,u=t.length;u>l;c++){o=i[c],a=t[l++],h=t[l++];var f=d(o.childNodes);a&&a(e,o,n,r,s),h&&h(e,f,n,r,s)}}}function Ge(t,e){var i=t.tagName.toLowerCase();if(!jn.test(i)){var n=gt(e,"elementDirectives",i);return n?Ke(t,i,"",e,n):void 0}}function Ze(t,e){var i=lt(t,e);if(i){var n=rt(t),r={name:"component",ref:n,expression:i.id,def:Hs.component,modifiers:{literal:!i.dynamic}},s=function(t,e,i,s,o){n&&kt((s||t).$refs,n,null),t._bindDir(r,e,i,s,o)};return s.terminal=!0,s}}function Xe(t,e,i){if(null!==I(t,"v-pre"))return Ye;if(t.hasAttribute("v-else")){var n=t.previousElementSibling;if(n&&n.hasAttribute("v-if"))return Ye}for(var r,s,o,a,h,l,c,u,f,p,d=0,v=e.length;v>d;d++)r=e[d],s=r.name.replace(Bs,""),(h=s.match(Vs))&&(f=gt(i,"directives",h[1]),f&&f.terminal&&(!p||(f.priority||Us)>p.priority)&&(p=f,c=r.name,a=ei(r.name),o=r.value,l=h[1],u=h[2]));return p?Ke(t,l,o,i,p,c,u,a):void 0}function Ye(){}function Ke(t,e,i,n,r,s,o,a){var h=A(i),l={name:e,arg:o,expression:h.expression,filters:h.filters,raw:i,attr:s,modifiers:a,def:r};"for"!==e&&"router-view"!==e||(l.ref=rt(t));var c=function(t,e,i,n,r){l.ref&&kt((n||t).$refs,l.ref,null),t._bindDir(l,e,i,n,r)};return c.terminal=!0,c}function ti(t,e){function i(t,e,i){var n=i&&ni(i),r=!n&&A(s);v.push({name:t,attr:o,raw:a,def:e,arg:l,modifiers:c,expression:r&&r.expression,filters:r&&r.filters,interp:i,hasOneTime:n})}for(var n,r,s,o,a,h,l,c,u,f,p,d=t.length,v=[];d--;)if(n=t[d],r=o=n.name,s=a=n.value,f=N(s),l=null,c=ei(r),r=r.replace(Bs,""),f)s=j(f),l=r,i("bind",bs.bind,f);else if(Ws.test(r))c.literal=!Is.test(r),i("transition",Hs.transition);else if(Ms.test(r))l=r.replace(Ms,""),i("on",bs.on);else if(Is.test(r))h=r.replace(Is,""),"style"===h||"class"===h?i(h,Hs[h]):(l=h,i("bind",bs.bind));else if(p=r.match(Vs)){if(h=p[1],l=p[2],"else"===h)continue;u=gt(e,"directives",h,!0),u&&i(h,u)}return v.length?ii(v):void 0}function ei(t){var e=Object.create(null),i=t.match(Bs);if(i)for(var n=i.length;n--;)e[i[n].slice(1)]=!0;return e}function ii(t){return function(e,i,n,r,s){for(var o=t.length;o--;)e._bindDir(t[o],i,n,r,s)}}function ni(t){for(var e=t.length;e--;)if(t[e].oneTime)return!0}function ri(t){return"SCRIPT"===t.tagName&&(!t.hasAttribute("type")||"text/javascript"===t.getAttribute("type"))}function si(t,e){return e&&(e._containerAttrs=ai(t)),it(t)&&(t=Xt(t)),e&&(e._asComponent&&!e.template&&(e.template="<slot></slot>"),e.template&&(e._content=K(t),t=oi(t,e))),at(t)&&(U(nt("v-start",!0),t),t.appendChild(nt("v-end",!0))),t}function oi(t,e){var i=e.template,n=Xt(i,!0);if(n){var r=n.firstChild,s=r.tagName&&r.tagName.toLowerCase();return e.replace?(t===document.body,n.childNodes.length>1||1!==r.nodeType||"component"===s||gt(e,"components",s)||V(r,"is")||gt(e,"elementDirectives",s)||r.hasAttribute("v-for")||r.hasAttribute("v-if")?n:(e._replacerAttrs=ai(r),hi(t,r),r)):(t.appendChild(n),t)}}function ai(t){return 1===t.nodeType&&t.hasAttributes()?d(t.attributes):void 0}function hi(t,e){for(var i,n,r=t.attributes,s=r.length;s--;)i=r[s].name,n=r[s].value,e.hasAttribute(i)||Js.test(i)?"class"===i&&!N(n)&&(n=n.trim())&&n.split(/\s+/).forEach(function(t){X(e,t)}):e.setAttribute(i,n)}function li(t,e){if(e){for(var i,n,r=t._slotContents=Object.create(null),s=0,o=e.children.length;o>s;s++)i=e.children[s],(n=i.getAttribute("slot"))&&(r[n]||(r[n]=[])).push(i);for(n in r)r[n]=ci(r[n],e);if(e.hasChildNodes()){var a=e.childNodes;if(1===a.length&&3===a[0].nodeType&&!a[0].data.trim())return;r["default"]=ci(e.childNodes,e)}}}function ci(t,e){var i=document.createDocumentFragment();t=d(t);for(var n=0,r=t.length;r>n;n++){var s=t[n];!it(s)||s.hasAttribute("v-if")||s.hasAttribute("v-for")||(e.removeChild(s),s=Xt(s,!0)),i.appendChild(s)}return i}function ui(t){function e(){}function n(t,e){var i=new Ut(e,t,null,{lazy:!0});return function(){return i.dirty&&i.evaluate(),_t.target&&i.depend(),i.value}}Object.defineProperty(t.prototype,"$data",{get:function(){return this._data},set:function(t){t!==this._data&&this._setData(t)}}),t.prototype._initState=function(){this._initProps(),this._initMeta(),this._initMethods(),this._initData(),this._initComputed()},t.prototype._initProps=function(){var t=this.$options,e=t.el,i=t.props;e=t.el=L(e),this._propsUnlinkFn=e&&1===e.nodeType&&i?Ie(this,e,i,this._scope):null},t.prototype._initData=function(){var t=this.$options.data,e=this._data=t?t():{};g(e)||(e={});var n,r,s=this._props,o=Object.keys(e);for(n=o.length;n--;)r=o[n],s&&i(s,r)||this._proxy(r);$t(e,this)},t.prototype._setData=function(t){t=t||{};var e=this._data;this._data=t;var n,r,s;for(n=Object.keys(e),s=n.length;s--;)r=n[s],r in t||this._unproxy(r);for(n=Object.keys(t),s=n.length;s--;)r=n[s],i(this,r)||this._proxy(r);e.__ob__.removeVm(this),$t(t,this),this._digest()},t.prototype._proxy=function(t){if(!r(t)){var e=this;Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(i){e._data[t]=i}})}},t.prototype._unproxy=function(t){r(t)||delete this[t]},t.prototype._digest=function(){for(var t=0,e=this._watchers.length;e>t;t++)this._watchers[t].update(!0)},t.prototype._initComputed=function(){var t=this.$options.computed;if(t)for(var i in t){var r=t[i],s={enumerable:!0,configurable:!0};"function"==typeof r?(s.get=n(r,this),s.set=e):(s.get=r.get?r.cache!==!1?n(r.get,this):p(r.get,this):e,s.set=r.set?p(r.set,this):e),Object.defineProperty(this,i,s)}},t.prototype._initMethods=function(){var t=this.$options.methods;if(t)for(var e in t)this[e]=p(t[e],this)},t.prototype._initMeta=function(){var t=this.$options._meta;if(t)for(var e in t)kt(this,e,t[e])}}function fi(t){function e(t,e){for(var i,n,r,s=e.attributes,o=0,a=s.length;a>o;o++)i=s[o].name,Qs.test(i)&&(i=i.replace(Qs,""),n=s[o].value,Mt(n)&&(n+=".apply(this, $arguments)"),r=(t._scope||t._context).$eval(n,!0),r._fromParent=!0,t.$on(i.replace(Qs),r))}function i(t,e,i){if(i){var r,s,o,a;for(s in i)if(r=i[s],Di(r))for(o=0,a=r.length;a>o;o++)n(t,e,s,r[o]);else n(t,e,s,r)}}function n(t,e,i,r,s){var o=typeof r;if("function"===o)t[e](i,r,s);else if("string"===o){var a=t.$options.methods,h=a&&a[r];h&&t[e](i,h,s)}else r&&"object"===o&&n(t,e,i,r.handler,r)}function r(){this._isAttached||(this._isAttached=!0,this.$children.forEach(s))}function s(t){!t._isAttached&&H(t.$el)&&t._callHook("attached")}function o(){this._isAttached&&(this._isAttached=!1,this.$children.forEach(a))}function a(t){t._isAttached&&!H(t.$el)&&t._callHook("detached")}t.prototype._initEvents=function(){var t=this.$options;t._asComponent&&e(this,t.el),i(this,"$on",t.events),i(this,"$watch",t.watch)},t.prototype._initDOMHooks=function(){this.$on("hook:attached",r),this.$on("hook:detached",o)},t.prototype._callHook=function(t){this.$emit("pre-hook:"+t);var e=this.$options[t];if(e)for(var i=0,n=e.length;n>i;i++)e[i].call(this);this.$emit("hook:"+t)}}function pi(){}function di(t,e,i,n,r,s){this.vm=e,this.el=i,this.descriptor=t,this.name=t.name,this.expression=t.expression,this.arg=t.arg,this.modifiers=t.modifiers,this.filters=t.filters,this.literal=this.modifiers&&this.modifiers.literal,this._locked=!1,this._bound=!1,this._listeners=null,this._host=n,this._scope=r,this._frag=s}function vi(t){t.prototype._updateRef=function(t){var e=this.$options._ref;if(e){var i=(this._scope||this._context).$refs;t?i[e]===this&&(i[e]=null):i[e]=this}},t.prototype._compile=function(t){var e=this.$options,i=t;if(t=si(t,e),this._initElement(t),1!==t.nodeType||null===I(t,"v-pre")){var n=this._context&&this._context.$options,r=Me(t,e,n);li(this,e._content);var s,o=this.constructor;e._linkerCachable&&(s=o.linker,s||(s=o.linker=De(t,e)));var a=r(this,t,this._scope),h=s?s(this,t):De(t,e)(this,t);this._unlinkFn=function(){a(),h(!0)},e.replace&&J(i,t),this._isCompiled=!0,this._callHook("compiled")}},t.prototype._initElement=function(t){at(t)?(this._isFragment=!0,this.$el=this._fragmentStart=t.firstChild,this._fragmentEnd=t.lastChild,3===this._fragmentStart.nodeType&&(this._fragmentStart.data=this._fragmentEnd.data=""),this._fragment=t):this.$el=t,this.$el.__vue__=this,this._callHook("beforeCompile")},t.prototype._bindDir=function(t,e,i,n,r){this._directives.push(new di(t,this,e,i,n,r))},t.prototype._destroy=function(t,e){if(this._isBeingDestroyed)return void(e||this._cleanup());var i,n,r=this,s=function(){!i||n||e||r._cleanup()};t&&this.$el&&(n=!0,this.$remove(function(){
-n=!1,s()})),this._callHook("beforeDestroy"),this._isBeingDestroyed=!0;var o,a=this.$parent;for(a&&!a._isBeingDestroyed&&(a.$children.$remove(this),this._updateRef(!0)),o=this.$children.length;o--;)this.$children[o].$destroy();for(this._propsUnlinkFn&&this._propsUnlinkFn(),this._unlinkFn&&this._unlinkFn(),o=this._watchers.length;o--;)this._watchers[o].teardown();this.$el&&(this.$el.__vue__=null),i=!0,s()},t.prototype._cleanup=function(){this._isDestroyed||(this._frag&&this._frag.children.$remove(this),this._data&&this._data.__ob__&&this._data.__ob__.removeVm(this),this.$el=this.$parent=this.$root=this.$children=this._watchers=this._context=this._scope=this._directives=null,this._isDestroyed=!0,this._callHook("destroyed"),this.$off())}}function mi(t){t.prototype._applyFilters=function(t,e,i,n){var r,s,o,a,h,l,c,u,f;for(l=0,c=i.length;c>l;l++)if(r=i[n?c-l-1:l],s=gt(this.$options,"filters",r.name,!0),s&&(s=n?s.write:s.read||s,"function"==typeof s)){if(o=n?[t,e]:[t],h=n?2:1,r.args)for(u=0,f=r.args.length;f>u;u++)a=r.args[u],o[u+h]=a.dynamic?this.$get(a.value):a.value;t=s.apply(this,o)}return t},t.prototype._resolveComponent=function(e,i){var n;if(n="function"==typeof e?e:gt(this.$options,"components",e,!0))if(n.options)i(n);else if(n.resolved)i(n.resolved);else if(n.requested)n.pendingCallbacks.push(i);else{n.requested=!0;var r=n.pendingCallbacks=[i];n.call(this,function(e){g(e)&&(e=t.extend(e)),n.resolved=e;for(var i=0,s=r.length;s>i;i++)r[i](e)},function(t){})}}}function gi(t){function i(t){return JSON.parse(JSON.stringify(t))}t.prototype.$get=function(t,e){var i=It(t);if(i){if(e){var n=this;return function(){n.$arguments=d(arguments);var t=i.get.call(n,n);return n.$arguments=null,t}}try{return i.get.call(this,this)}catch(r){}}},t.prototype.$set=function(t,e){var i=It(t,!0);i&&i.set&&i.set.call(this,this,e)},t.prototype.$delete=function(t){e(this._data,t)},t.prototype.$watch=function(t,e,i){var n,r=this;"string"==typeof t&&(n=A(t),t=n.expression);var s=new Ut(r,t,e,{deep:i&&i.deep,sync:i&&i.sync,filters:n&&n.filters,user:!i||i.user!==!1});return i&&i.immediate&&e.call(r,s.value),function(){s.teardown()}},t.prototype.$eval=function(t,e){if(Gs.test(t)){var i=A(t),n=this.$get(i.expression,e);return i.filters?this._applyFilters(n,null,i.filters):n}return this.$get(t,e)},t.prototype.$interpolate=function(t){var e=N(t),i=this;return e?1===e.length?i.$eval(e[0].value)+"":e.map(function(t){return t.tag?i.$eval(t.value):t.value}).join(""):t},t.prototype.$log=function(t){var e=t?jt(this._data,t):this._data;if(e&&(e=i(e)),!t){var n;for(n in this.$options.computed)e[n]=i(this[n]);if(this._props)for(n in this._props)e[n]=i(this[n])}console.log(e)}}function _i(t){function e(t,e,n,r,s,o){e=i(e);var a=!H(e),h=r===!1||a?s:o,l=!a&&!t._isAttached&&!H(t.$el);return t._isFragment?(st(t._fragmentStart,t._fragmentEnd,function(i){h(i,e,t)}),n&&n()):h(t.$el,e,t,n),l&&t._callHook("attached"),t}function i(t){return"string"==typeof t?document.querySelector(t):t}function n(t,e,i,n){e.appendChild(t),n&&n()}function r(t,e,i,n){B(t,e),n&&n()}function s(t,e,i){z(t),i&&i()}t.prototype.$nextTick=function(t){Yi(t,this)},t.prototype.$appendTo=function(t,i,r){return e(this,t,i,r,n,F)},t.prototype.$prependTo=function(t,e,n){return t=i(t),t.hasChildNodes()?this.$before(t.firstChild,e,n):this.$appendTo(t,e,n),this},t.prototype.$before=function(t,i,n){return e(this,t,i,n,r,D)},t.prototype.$after=function(t,e,n){return t=i(t),t.nextSibling?this.$before(t.nextSibling,e,n):this.$appendTo(t.parentNode,e,n),this},t.prototype.$remove=function(t,e){if(!this.$el.parentNode)return t&&t();var i=this._isAttached&&H(this.$el);i||(e=!1);var n=this,r=function(){i&&n._callHook("detached"),t&&t()};if(this._isFragment)ot(this._fragmentStart,this._fragmentEnd,this,this._fragment,r);else{var o=e===!1?s:P;o(this.$el,this,r)}return this}}function yi(t){function e(t,e,n){var r=t.$parent;if(r&&n&&!i.test(e))for(;r;)r._eventsCount[e]=(r._eventsCount[e]||0)+n,r=r.$parent}t.prototype.$on=function(t,i){return(this._events[t]||(this._events[t]=[])).push(i),e(this,t,1),this},t.prototype.$once=function(t,e){function i(){n.$off(t,i),e.apply(this,arguments)}var n=this;return i.fn=e,this.$on(t,i),this},t.prototype.$off=function(t,i){var n;if(!arguments.length){if(this.$parent)for(t in this._events)n=this._events[t],n&&e(this,t,-n.length);return this._events={},this}if(n=this._events[t],!n)return this;if(1===arguments.length)return e(this,t,-n.length),this._events[t]=null,this;for(var r,s=n.length;s--;)if(r=n[s],r===i||r.fn===i){e(this,t,-1),n.splice(s,1);break}return this},t.prototype.$emit=function(t){var e="string"==typeof t;t=e?t:t.name;var i=this._events[t],n=e||!i;if(i){i=i.length>1?d(i):i;var r=e&&i.some(function(t){return t._fromParent});r&&(n=!1);for(var s=d(arguments,1),o=0,a=i.length;a>o;o++){var h=i[o],l=h.apply(this,s);l!==!0||r&&!h._fromParent||(n=!0)}}return n},t.prototype.$broadcast=function(t){var e="string"==typeof t;if(t=e?t:t.name,this._eventsCount[t]){var i=this.$children,n=d(arguments);e&&(n[0]={name:t,source:this});for(var r=0,s=i.length;s>r;r++){var o=i[r],a=o.$emit.apply(o,n);a&&o.$broadcast.apply(o,n)}return this}},t.prototype.$dispatch=function(t){var e=this.$emit.apply(this,arguments);if(e){var i=this.$parent,n=d(arguments);for(n[0]={name:t,source:this};i;)e=i.$emit.apply(i,n),i=e?i.$parent:null;return this}};var i=/^hook:/}function bi(t){function e(){this._isAttached=!0,this._isReady=!0,this._callHook("ready")}t.prototype.$mount=function(t){return this._isCompiled?void 0:(t=L(t),t||(t=document.createElement("div")),this._compile(t),this._initDOMHooks(),H(this.$el)?(this._callHook("attached"),e.call(this)):this.$once("hook:attached",e),this)},t.prototype.$destroy=function(t,e){this._destroy(t,e)},t.prototype.$compile=function(t,e,i,n){return De(t,this.$options,!0)(this,t,e,i,n)}}function wi(t){this._init(t)}function Ci(t,e,i){return i=i?parseInt(i,10):0,e=o(e),"number"==typeof e?t.slice(i,i+e):t}function $i(t,e,i){if(t=Ks(t),null==e)return t;if("function"==typeof e)return t.filter(e);e=(""+e).toLowerCase();for(var n,r,s,o,a="in"===i?3:2,h=Array.prototype.concat.apply([],d(arguments,a)),l=[],c=0,u=t.length;u>c;c++)if(n=t[c],s=n&&n.$value||n,o=h.length){for(;o--;)if(r=h[o],"$key"===r&&xi(n.$key,e)||xi(jt(s,r),e)){l.push(n);break}}else xi(n,e)&&l.push(n);return l}function ki(t){function e(t,e,i){var r=n[i];return r&&("$key"!==r&&(m(t)&&"$value"in t&&(t=t.$value),m(e)&&"$value"in e&&(e=e.$value)),t=m(t)?jt(t,r):t,e=m(e)?jt(e,r):e),t===e?0:t>e?s:-s}var i=null,n=void 0;t=Ks(t);var r=d(arguments,1),s=r[r.length-1];"number"==typeof s?(s=0>s?-1:1,r=r.length>1?r.slice(0,-1):r):s=1;var o=r[0];return o?("function"==typeof o?i=function(t,e){return o(t,e)*s}:(n=Array.prototype.concat.apply([],r),i=function(t,r,s){return s=s||0,s>=n.length-1?e(t,r,s):e(t,r,s)||i(t,r,s+1)}),t.slice().sort(i)):t}function xi(t,e){var i;if(g(t)){var n=Object.keys(t);for(i=n.length;i--;)if(xi(t[n[i]],e))return!0}else if(Di(t)){for(i=t.length;i--;)if(xi(t[i],e))return!0}else if(null!=t)return t.toString().toLowerCase().indexOf(e)>-1}function Ai(i){function n(t){return new Function("return function "+f(t)+" (options) { this._init(options) }")()}i.options={directives:bs,elementDirectives:Ys,filters:eo,transitions:{},components:{},partials:{},replace:!0},i.util=In,i.config=An,i.set=t,i["delete"]=e,i.nextTick=Yi,i.compiler=qs,i.FragmentFactory=se,i.internalDirectives=Hs,i.parsers={path:ir,text:$n,template:Fr,directive:gn,expression:mr},i.cid=0;var r=1;i.extend=function(t){t=t||{};var e=this,i=0===e.cid;if(i&&t._Ctor)return t._Ctor;var s=t.name||e.options.name,o=n(s||"VueComponent");return o.prototype=Object.create(e.prototype),o.prototype.constructor=o,o.cid=r++,o.options=mt(e.options,t),o["super"]=e,o.extend=e.extend,An._assetTypes.forEach(function(t){o[t]=e[t]}),s&&(o.options.components[s]=o),i&&(t._Ctor=o),o},i.use=function(t){if(!t.installed){var e=d(arguments,1);return e.unshift(this),"function"==typeof t.install?t.install.apply(t,e):t.apply(null,e),t.installed=!0,this}},i.mixin=function(t){i.options=mt(i.options,t)},An._assetTypes.forEach(function(t){i[t]=function(e,n){return n?("component"===t&&g(n)&&(n.name||(n.name=e),n=i.extend(n)),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}),v(i.transition,Tn)}var Oi=Object.prototype.hasOwnProperty,Ti=/^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/,Ni=/-(\w)/g,ji=/([a-z\d])([A-Z])/g,Ei=/(?:^|[-_\/])(\w)/g,Si=Object.prototype.toString,Fi="[object Object]",Di=Array.isArray,Pi="__proto__"in{},Ri="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Li=Ri&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Hi=Ri&&window.navigator.userAgent.toLowerCase(),Ii=Hi&&Hi.indexOf("trident")>0,Mi=Hi&&Hi.indexOf("msie 9.0")>0,Vi=Hi&&Hi.indexOf("android")>0,Bi=Hi&&/(iphone|ipad|ipod|ios)/i.test(Hi),Wi=Bi&&Hi.match(/os ([\d_]+)/),zi=Wi&&Wi[1].split("_"),Ui=zi&&Number(zi[0])>=9&&Number(zi[1])>=3&&!window.indexedDB,Ji=void 0,qi=void 0,Qi=void 0,Gi=void 0;if(Ri&&!Mi){var Zi=void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend,Xi=void 0===window.onanimationend&&void 0!==window.onwebkitanimationend;Ji=Zi?"WebkitTransition":"transition",qi=Zi?"webkitTransitionEnd":"transitionend",Qi=Xi?"WebkitAnimation":"animation",Gi=Xi?"webkitAnimationEnd":"animationend"}var Yi=function(){function t(){n=!1;var t=i.slice(0);i=[];for(var e=0;e<t.length;e++)t[e]()}var e,i=[],n=!1;if("undefined"==typeof MutationObserver||Ui){var r=Ri?window:"undefined"!=typeof global?global:{};e=r.setImmediate||setTimeout}else{var s=1,o=new MutationObserver(t),a=document.createTextNode(s);o.observe(a,{characterData:!0}),e=function(){s=(s+1)%2,a.data=s}}return function(r,s){var o=s?function(){r.call(s)}:r;i.push(o),n||(n=!0,e(t,0))}}(),Ki=void 0;"undefined"!=typeof Set&&Set.toString().match(/native code/)?Ki=Set:(Ki=function(){this.set=Object.create(null)},Ki.prototype.has=function(t){return void 0!==this.set[t]},Ki.prototype.add=function(t){this.set[t]=1},Ki.prototype.clear=function(){this.set=Object.create(null)});var tn=$.prototype;tn.put=function(t,e){var i,n=this.get(t,!0);return n||(this.size===this.limit&&(i=this.shift()),n={key:t},this._keymap[t]=n,this.tail?(this.tail.newer=n,n.older=this.tail):this.head=n,this.tail=n,this.size++),n.value=e,i},tn.shift=function(){var t=this.head;return t&&(this.head=this.head.newer,this.head.older=void 0,t.newer=t.older=void 0,this._keymap[t.key]=void 0,this.size--),t},tn.get=function(t,e){var i=this._keymap[t];if(void 0!==i)return i===this.tail?e?i:i.value:(i.newer&&(i===this.head&&(this.head=i.newer),i.newer.older=i.older),i.older&&(i.older.newer=i.newer),i.newer=void 0,i.older=this.tail,this.tail&&(this.tail.newer=i),this.tail=i,e?i:i.value)};var en,nn,rn,sn,on,an,hn,ln,cn,un,fn,pn,dn=new $(1e3),vn=/[^\s'"]+|'[^']*'|"[^"]*"/g,mn=/^in$|^-?\d+/,gn=Object.freeze({parseDirective:A}),_n=/[-.*+?^${}()|[\]\/\\]/g,yn=void 0,bn=void 0,wn=void 0,Cn=/[^|]\|[^|]/,$n=Object.freeze({compileRegex:T,parseText:N,tokensToExp:j}),kn=["{{","}}"],xn=["{{{","}}}"],An=Object.defineProperties({debug:!1,silent:!1,async:!0,warnExpressionErrors:!0,devtools:!1,_delimitersChanged:!0,_assetTypes:["component","directive","elementDirective","filter","transition","partial"],_propBindingModes:{ONE_WAY:0,TWO_WAY:1,ONE_TIME:2},_maxUpdateCount:100},{delimiters:{get:function(){return kn},set:function(t){kn=t,T()},configurable:!0,enumerable:!0},unsafeDelimiters:{get:function(){return xn},set:function(t){xn=t,T()},configurable:!0,enumerable:!0}}),On=void 0,Tn=Object.freeze({appendWithTransition:F,beforeWithTransition:D,removeWithTransition:P,applyTransition:R}),Nn=/^v-ref:/,jn=/^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i,En=/^(slot|partial|component)$/i,Sn=An.optionMergeStrategies=Object.create(null);Sn.data=function(t,e,i){return i?t||e?function(){var n="function"==typeof e?e.call(i):e,r="function"==typeof t?t.call(i):void 0;return n?ut(n,r):r}:void 0:e?"function"!=typeof e?t:t?function(){return ut(e.call(this),t.call(this))}:e:t},Sn.el=function(t,e,i){if(i||!e||"function"==typeof e){var n=e||t;return i&&"function"==typeof n?n.call(i):n}},Sn.init=Sn.created=Sn.ready=Sn.attached=Sn.detached=Sn.beforeCompile=Sn.compiled=Sn.beforeDestroy=Sn.destroyed=Sn.activate=function(t,e){return e?t?t.concat(e):Di(e)?e:[e]:t},An._assetTypes.forEach(function(t){Sn[t+"s"]=ft}),Sn.watch=Sn.events=function(t,e){if(!e)return t;if(!t)return e;var i={};v(i,t);for(var n in e){var r=i[n],s=e[n];r&&!Di(r)&&(r=[r]),i[n]=r?r.concat(s):[s]}return i},Sn.props=Sn.methods=Sn.computed=function(t,e){if(!e)return t;if(!t)return e;var i=Object.create(null);return v(i,t),v(i,e),i};var Fn=function(t,e){return void 0===e?t:e},Dn=0;_t.target=null,_t.prototype.addSub=function(t){this.subs.push(t)},_t.prototype.removeSub=function(t){this.subs.$remove(t)},_t.prototype.depend=function(){_t.target.addDep(this)},_t.prototype.notify=function(){for(var t=d(this.subs),e=0,i=t.length;i>e;e++)t[e].update()};var Pn=Array.prototype,Rn=Object.create(Pn);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(t){var e=Pn[t];_(Rn,t,function(){for(var i=arguments.length,n=new Array(i);i--;)n[i]=arguments[i];var r,s=e.apply(this,n),o=this.__ob__;switch(t){case"push":r=n;break;case"unshift":r=n;break;case"splice":r=n.slice(2)}return r&&o.observeArray(r),o.dep.notify(),s})}),_(Pn,"$set",function(t,e){return t>=this.length&&(this.length=Number(t)+1),this.splice(t,1,e)[0]}),_(Pn,"$remove",function(t){if(this.length){var e=b(this,t);return e>-1?this.splice(e,1):void 0}});var Ln=Object.getOwnPropertyNames(Rn),Hn=!0;bt.prototype.walk=function(t){for(var e=Object.keys(t),i=0,n=e.length;n>i;i++)this.convert(e[i],t[e[i]])},bt.prototype.observeArray=function(t){for(var e=0,i=t.length;i>e;e++)$t(t[e])},bt.prototype.convert=function(t,e){kt(this.value,t,e)},bt.prototype.addVm=function(t){(this.vms||(this.vms=[])).push(t)},bt.prototype.removeVm=function(t){this.vms.$remove(t)};var In=Object.freeze({defineReactive:kt,set:t,del:e,hasOwn:i,isLiteral:n,isReserved:r,_toString:s,toNumber:o,toBoolean:a,stripQuotes:h,camelize:l,hyphenate:u,classify:f,bind:p,toArray:d,extend:v,isObject:m,isPlainObject:g,def:_,debounce:y,indexOf:b,cancellable:w,looseEqual:C,isArray:Di,hasProto:Pi,inBrowser:Ri,devtools:Li,isIE:Ii,isIE9:Mi,isAndroid:Vi,isIos:Bi,iosVersionMatch:Wi,iosVersion:zi,hasMutationObserverBug:Ui,get transitionProp(){return Ji},get transitionEndEvent(){return qi},get animationProp(){return Qi},get animationEndEvent(){return Gi},nextTick:Yi,get _Set(){return Ki},query:L,inDoc:H,getAttr:I,getBindAttr:M,hasBindAttr:V,before:B,after:W,remove:z,prepend:U,replace:J,on:q,off:Q,setClass:Z,addClass:X,removeClass:Y,extractContent:K,trimNode:tt,isTemplate:it,createAnchor:nt,findRef:rt,mapNodeRange:st,removeNodeRange:ot,isFragment:at,getOuterHTML:ht,mergeOptions:mt,resolveAsset:gt,checkComponentAttr:lt,commonTagRE:jn,reservedTagRE:En,warn:On}),Mn=0,Vn=new $(1e3),Bn=0,Wn=1,zn=2,Un=3,Jn=0,qn=1,Qn=2,Gn=3,Zn=4,Xn=5,Yn=6,Kn=7,tr=8,er=[];er[Jn]={ws:[Jn],ident:[Gn,Bn],"[":[Zn],eof:[Kn]},er[qn]={ws:[qn],".":[Qn],"[":[Zn],eof:[Kn]},er[Qn]={ws:[Qn],ident:[Gn,Bn]},er[Gn]={ident:[Gn,Bn],0:[Gn,Bn],number:[Gn,Bn],ws:[qn,Wn],".":[Qn,Wn],"[":[Zn,Wn],eof:[Kn,Wn]},er[Zn]={"'":[Xn,Bn],'"':[Yn,Bn],"[":[Zn,zn],"]":[qn,Un],eof:tr,"else":[Zn,Bn]},er[Xn]={"'":[Zn,Bn],eof:tr,"else":[Xn,Bn]},er[Yn]={'"':[Zn,Bn],eof:tr,"else":[Yn,Bn]};var ir=Object.freeze({parsePath:Nt,getPath:jt,setPath:Et}),nr=new $(1e3),rr="Math,Date,this,true,false,null,undefined,Infinity,NaN,isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,parseInt,parseFloat",sr=new RegExp("^("+rr.replace(/,/g,"\\b|")+"\\b)"),or="break,case,class,catch,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,let,return,super,switch,throw,try,var,while,with,yield,enum,await,implements,package,protected,static,interface,private,public",ar=new RegExp("^("+or.replace(/,/g,"\\b|")+"\\b)"),hr=/\s/g,lr=/\n/g,cr=/[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g,ur=/"(\d+)"/g,fr=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/,pr=/[^\w$\.](?:[A-Za-z_$][\w$]*)/g,dr=/^(?:true|false|null|undefined|Infinity|NaN)$/,vr=[],mr=Object.freeze({parseExpression:It,isSimplePath:Mt}),gr=[],_r=[],yr={},br={},wr=!1,Cr=0;Ut.prototype.get=function(){this.beforeGet();var t,e=this.scope||this.vm;try{t=this.getter.call(e,e)}catch(i){}return this.deep&&Jt(t),this.preProcess&&(t=this.preProcess(t)),this.filters&&(t=e._applyFilters(t,null,this.filters,!1)),this.postProcess&&(t=this.postProcess(t)),this.afterGet(),t},Ut.prototype.set=function(t){var e=this.scope||this.vm;this.filters&&(t=e._applyFilters(t,this.value,this.filters,!0));try{this.setter.call(e,e,t)}catch(i){}var n=e.$forContext;if(n&&n.alias===this.expression){if(n.filters)return;n._withLock(function(){e.$key?n.rawValue[e.$key]=t:n.rawValue.$set(e.$index,t)})}},Ut.prototype.beforeGet=function(){_t.target=this},Ut.prototype.addDep=function(t){var e=t.id;this.newDepIds.has(e)||(this.newDepIds.add(e),this.newDeps.push(t),this.depIds.has(e)||t.addSub(this))},Ut.prototype.afterGet=function(){_t.target=null;for(var t=this.deps.length;t--;){var e=this.deps[t];this.newDepIds.has(e.id)||e.removeSub(this)}var i=this.depIds;this.depIds=this.newDepIds,this.newDepIds=i,this.newDepIds.clear(),i=this.deps,this.deps=this.newDeps,this.newDeps=i,this.newDeps.length=0},Ut.prototype.update=function(t){this.lazy?this.dirty=!0:this.sync||!An.async?this.run():(this.shallow=this.queued?t?this.shallow:!1:!!t,this.queued=!0,zt(this))},Ut.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||(m(t)||this.deep)&&!this.shallow){var e=this.value;this.value=t;this.prevError;this.cb.call(this.vm,t,e)}this.queued=this.shallow=!1}},Ut.prototype.evaluate=function(){var t=_t.target;this.value=this.get(),this.dirty=!1,_t.target=t},Ut.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},Ut.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||this.vm._watchers.$remove(this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1,this.vm=this.cb=this.value=null}};var $r=new Ki,kr={bind:function(){this.attr=3===this.el.nodeType?"data":"textContent"},update:function(t){this.el[this.attr]=s(t)}},xr=new $(1e3),Ar=new $(1e3),Or={efault:[0,"",""],legend:[1,"<fieldset>","</fieldset>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]};Or.td=Or.th=[3,"<table><tbody><tr>","</tr></tbody></table>"],Or.option=Or.optgroup=[1,'<select multiple="multiple">',"</select>"],Or.thead=Or.tbody=Or.colgroup=Or.caption=Or.tfoot=[1,"<table>","</table>"],Or.g=Or.defs=Or.symbol=Or.use=Or.image=Or.text=Or.circle=Or.ellipse=Or.line=Or.path=Or.polygon=Or.polyline=Or.rect=[1,'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"version="1.1">',"</svg>"];var Tr=/<([\w:-]+)/,Nr=/&#?\w+?;/,jr=/<!--/,Er=function(){if(Ri){var t=document.createElement("div");return t.innerHTML="<template>1</template>",!t.cloneNode(!0).firstChild.innerHTML}return!1}(),Sr=function(){if(Ri){var t=document.createElement("textarea");return t.placeholder="t","t"===t.cloneNode(!0).value}return!1}(),Fr=Object.freeze({cloneNode:Zt,parseTemplate:Xt}),Dr={bind:function(){8===this.el.nodeType&&(this.nodes=[],this.anchor=nt("v-html"),J(this.el,this.anchor))},update:function(t){t=s(t),this.nodes?this.swap(t):this.el.innerHTML=t},swap:function(t){for(var e=this.nodes.length;e--;)z(this.nodes[e]);var i=Xt(t,!0,!0);this.nodes=d(i.childNodes),B(i,this.anchor)}};Yt.prototype.callHook=function(t){var e,i;for(e=0,i=this.childFrags.length;i>e;e++)this.childFrags[e].callHook(t);for(e=0,i=this.children.length;i>e;e++)t(this.children[e])},Yt.prototype.beforeRemove=function(){var t,e;for(t=0,e=this.childFrags.length;e>t;t++)this.childFrags[t].beforeRemove(!1);for(t=0,e=this.children.length;e>t;t++)this.children[t].$destroy(!1,!0);var i=this.unlink.dirs;for(t=0,e=i.length;e>t;t++)i[t]._watcher&&i[t]._watcher.teardown()},Yt.prototype.destroy=function(){this.parentFrag&&this.parentFrag.childFrags.$remove(this),this.node.__v_frag=null,this.unlink()};var Pr=new $(5e3);se.prototype.create=function(t,e,i){var n=Zt(this.template);return new Yt(this.linker,this.vm,n,t,e,i)};var Rr=700,Lr=800,Hr=850,Ir=1100,Mr=1500,Vr=1500,Br=1750,Wr=2100,zr=2200,Ur=2300,Jr=0,qr={priority:zr,terminal:!0,params:["track-by","stagger","enter-stagger","leave-stagger"],bind:function(){var t=this.expression.match(/(.*) (?:in|of) (.*)/);if(t){var e=t[1].match(/\((.*),(.*)\)/);e?(this.iterator=e[1].trim(),this.alias=e[2].trim()):this.alias=t[1].trim(),this.expression=t[2]}if(this.alias){this.id="__v-for__"+ ++Jr;var i=this.el.tagName;this.isOption=("OPTION"===i||"OPTGROUP"===i)&&"SELECT"===this.el.parentNode.tagName,this.start=nt("v-for-start"),this.end=nt("v-for-end"),J(this.el,this.end),B(this.start,this.end),this.cache=Object.create(null),this.factory=new se(this.vm,this.el)}},update:function(t){this.diff(t),this.updateRef(),this.updateModel()},diff:function(t){var e,n,r,s,o,a,h=t[0],l=this.fromObject=m(h)&&i(h,"$key")&&i(h,"$value"),c=this.params.trackBy,u=this.frags,f=this.frags=new Array(t.length),p=this.alias,d=this.iterator,v=this.start,g=this.end,_=H(v),y=!u;for(e=0,n=t.length;n>e;e++)h=t[e],s=l?h.$key:null,o=l?h.$value:h,a=!m(o),r=!y&&this.getCachedFrag(o,e,s),r?(r.reused=!0,r.scope.$index=e,s&&(r.scope.$key=s),d&&(r.scope[d]=null!==s?s:e),(c||l||a)&&yt(function(){r.scope[p]=o})):(r=this.create(o,p,e,s),r.fresh=!y),f[e]=r,y&&r.before(g);if(!y){var b=0,w=u.length-f.length;for(this.vm._vForRemoving=!0,e=0,n=u.length;n>e;e++)r=u[e],r.reused||(this.deleteCachedFrag(r),this.remove(r,b++,w,_));this.vm._vForRemoving=!1,b&&(this.vm._watchers=this.vm._watchers.filter(function(t){return t.active}));var C,$,k,x=0;for(e=0,n=f.length;n>e;e++)r=f[e],C=f[e-1],$=C?C.staggerCb?C.staggerAnchor:C.end||C.node:v,r.reused&&!r.staggerCb?(k=oe(r,v,this.id),k===C||k&&oe(k,v,this.id)===C||this.move(r,$)):this.insert(r,x++,$,_),r.reused=r.fresh=!1}},create:function(t,e,i,n){var r=this._host,s=this._scope||this.vm,o=Object.create(s);o.$refs=Object.create(s.$refs),o.$els=Object.create(s.$els),o.$parent=s,o.$forContext=this,yt(function(){kt(o,e,t)}),kt(o,"$index",i),n?kt(o,"$key",n):o.$key&&_(o,"$key",null),this.iterator&&kt(o,this.iterator,null!==n?n:i);var a=this.factory.create(r,o,this._frag);return a.forId=this.id,this.cacheFrag(t,a,i,n),a},updateRef:function(){var t=this.descriptor.ref;if(t){var e,i=(this._scope||this.vm).$refs;this.fromObject?(e={},this.frags.forEach(function(t){e[t.scope.$key]=ae(t)})):e=this.frags.map(ae),i[t]=e}},updateModel:function(){if(this.isOption){var t=this.start.parentNode,e=t&&t.__v_model;e&&e.forceUpdate()}},insert:function(t,e,i,n){t.staggerCb&&(t.staggerCb.cancel(),t.staggerCb=null);var r=this.getStagger(t,e,null,"enter");if(n&&r){var s=t.staggerAnchor;s||(s=t.staggerAnchor=nt("stagger-anchor"),s.__v_frag=t),W(s,i);var o=t.staggerCb=w(function(){t.staggerCb=null,t.before(s),z(s)});setTimeout(o,r)}else{var a=i.nextSibling;a||(W(this.end,i),a=this.end),t.before(a)}},remove:function(t,e,i,n){if(t.staggerCb)return t.staggerCb.cancel(),void(t.staggerCb=null);var r=this.getStagger(t,e,i,"leave");if(n&&r){var s=t.staggerCb=w(function(){t.staggerCb=null,t.remove()});setTimeout(s,r)}else t.remove()},move:function(t,e){e.nextSibling||this.end.parentNode.appendChild(this.end),t.before(e.nextSibling,!1)},cacheFrag:function(t,e,n,r){var s,o=this.params.trackBy,a=this.cache,h=!m(t);r||o||h?(s=le(n,r,t,o),a[s]||(a[s]=e)):(s=this.id,i(t,s)?null===t[s]&&(t[s]=e):Object.isExtensible(t)&&_(t,s,e)),e.raw=t},getCachedFrag:function(t,e,i){var n,r=this.params.trackBy,s=!m(t);if(i||r||s){var o=le(e,i,t,r);n=this.cache[o]}else n=t[this.id];return n&&(n.reused||n.fresh),n},deleteCachedFrag:function(t){var e=t.raw,n=this.params.trackBy,r=t.scope,s=r.$index,o=i(r,"$key")&&r.$key,a=!m(e);if(n||o||a){var h=le(s,o,e,n);this.cache[h]=null}else e[this.id]=null,t.raw=null},getStagger:function(t,e,i,n){n+="Stagger";var r=t.node.__v_trans,s=r&&r.hooks,o=s&&(s[n]||s.stagger);return o?o.call(t,e,i):e*parseInt(this.params[n]||this.params.stagger,10)},_preProcess:function(t){return this.rawValue=t,t},_postProcess:function(t){if(Di(t))return t;if(g(t)){for(var e,i=Object.keys(t),n=i.length,r=new Array(n);n--;)e=i[n],r[n]={$key:e,$value:t[e]};return r}return"number"!=typeof t||isNaN(t)||(t=he(t)),t||[]},unbind:function(){if(this.descriptor.ref&&((this._scope||this.vm).$refs[this.descriptor.ref]=null),this.frags)for(var t,e=this.frags.length;e--;)t=this.frags[e],this.deleteCachedFrag(t),t.destroy()}},Qr={priority:Wr,terminal:!0,bind:function(){var t=this.el;if(t.__vue__)this.invalid=!0;else{var e=t.nextElementSibling;e&&null!==I(e,"v-else")&&(z(e),this.elseEl=e),this.anchor=nt("v-if"),J(t,this.anchor)}},update:function(t){this.invalid||(t?this.frag||this.insert():this.remove())},insert:function(){this.elseFrag&&(this.elseFrag.remove(),this.elseFrag=null),this.factory||(this.factory=new se(this.vm,this.el)),this.frag=this.factory.create(this._host,this._scope,this._frag),this.frag.before(this.anchor)},remove:function(){this.frag&&(this.frag.remove(),this.frag=null),this.elseEl&&!this.elseFrag&&(this.elseFactory||(this.elseFactory=new se(this.elseEl._context||this.vm,this.elseEl)),this.elseFrag=this.elseFactory.create(this._host,this._scope,this._frag),this.elseFrag.before(this.anchor))},unbind:function(){this.frag&&this.frag.destroy(),this.elseFrag&&this.elseFrag.destroy()}},Gr={bind:function(){var t=this.el.nextElementSibling;t&&null!==I(t,"v-else")&&(this.elseEl=t)},update:function(t){this.apply(this.el,t),this.elseEl&&this.apply(this.elseEl,!t)},apply:function(t,e){function i(){t.style.display=e?"":"none"}H(t)?R(t,e?1:-1,i,this.vm):i()}},Zr={bind:function(){var t=this,e=this.el,i="range"===e.type,n=this.params.lazy,r=this.params.number,s=this.params.debounce,a=!1;if(Vi||i||(this.on("compositionstart",function(){a=!0}),this.on("compositionend",function(){a=!1,n||t.listener()})),this.focused=!1,i||n||(this.on("focus",function(){t.focused=!0}),this.on("blur",function(){t.focused=!1,t._frag&&!t._frag.inserted||t.rawListener()})),this.listener=this.rawListener=function(){if(!a&&t._bound){var n=r||i?o(e.value):e.value;t.set(n),Yi(function(){t._bound&&!t.focused&&t.update(t._watcher.value)})}},s&&(this.listener=y(this.listener,s)),this.hasjQuery="function"==typeof jQuery,this.hasjQuery){var h=jQuery.fn.on?"on":"bind";jQuery(e)[h]("change",this.rawListener),n||jQuery(e)[h]("input",this.listener)}else this.on("change",this.rawListener),n||this.on("input",this.listener);!n&&Mi&&(this.on("cut",function(){Yi(t.listener)}),this.on("keyup",function(e){46!==e.keyCode&&8!==e.keyCode||t.listener()})),(e.hasAttribute("value")||"TEXTAREA"===e.tagName&&e.value.trim())&&(this.afterBind=this.listener)},update:function(t){t=s(t),t!==this.el.value&&(this.el.value=t)},unbind:function(){var t=this.el;if(this.hasjQuery){var e=jQuery.fn.off?"off":"unbind";jQuery(t)[e]("change",this.listener),jQuery(t)[e]("input",this.listener)}}},Xr={bind:function(){var t=this,e=this.el;this.getValue=function(){if(e.hasOwnProperty("_value"))return e._value;var i=e.value;return t.params.number&&(i=o(i)),i},this.listener=function(){t.set(t.getValue())},this.on("change",this.listener),e.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){this.el.checked=C(t,this.getValue())}},Yr={bind:function(){var t=this,e=this,i=this.el;this.forceUpdate=function(){e._watcher&&e.update(e._watcher.get())};var n=this.multiple=i.hasAttribute("multiple");this.listener=function(){var t=ce(i,n);t=e.params.number?Di(t)?t.map(o):o(t):t,e.set(t)},this.on("change",this.listener);var r=ce(i,n,!0);(n&&r.length||!n&&null!==r)&&(this.afterBind=this.listener),this.vm.$on("hook:attached",function(){Yi(t.forceUpdate)}),H(i)||Yi(this.forceUpdate)},update:function(t){var e=this.el;e.selectedIndex=-1;for(var i,n,r=this.multiple&&Di(t),s=e.options,o=s.length;o--;)i=s[o],n=i.hasOwnProperty("_value")?i._value:i.value,i.selected=r?ue(t,n)>-1:C(t,n)},unbind:function(){this.vm.$off("hook:attached",this.forceUpdate)}},Kr={bind:function(){function t(){var t=i.checked;return t&&i.hasOwnProperty("_trueValue")?i._trueValue:!t&&i.hasOwnProperty("_falseValue")?i._falseValue:t}var e=this,i=this.el;this.getValue=function(){return i.hasOwnProperty("_value")?i._value:e.params.number?o(i.value):i.value},this.listener=function(){var n=e._watcher.value;if(Di(n)){var r=e.getValue();i.checked?b(n,r)<0&&n.push(r):n.$remove(r)}else e.set(t())},this.on("change",this.listener),i.hasAttribute("checked")&&(this.afterBind=this.listener)},update:function(t){var e=this.el;Di(t)?e.checked=b(t,this.getValue())>-1:e.hasOwnProperty("_trueValue")?e.checked=C(t,e._trueValue):e.checked=!!t}},ts={text:Zr,radio:Xr,select:Yr,checkbox:Kr},es={priority:Lr,twoWay:!0,handlers:ts,params:["lazy","number","debounce"],bind:function(){this.checkFilters(),this.hasRead&&!this.hasWrite;var t,e=this.el,i=e.tagName;if("INPUT"===i)t=ts[e.type]||ts.text;else if("SELECT"===i)t=ts.select;else{if("TEXTAREA"!==i)return;t=ts.text}e.__v_model=this,t.bind.call(this),this.update=t.update,this._unbind=t.unbind},checkFilters:function(){var t=this.filters;if(t)for(var e=t.length;e--;){var i=gt(this.vm.$options,"filters",t[e].name);("function"==typeof i||i.read)&&(this.hasRead=!0),i.write&&(this.hasWrite=!0)}},unbind:function(){this.el.__v_model=null,this._unbind&&this._unbind()}},is={esc:27,tab:9,enter:13,space:32,"delete":[8,46],up:38,left:37,right:39,down:40},ns={priority:Rr,acceptStatement:!0,keyCodes:is,bind:function(){if("IFRAME"===this.el.tagName&&"load"!==this.arg){var t=this;this.iframeBind=function(){q(t.el.contentWindow,t.arg,t.handler,t.modifiers.capture)},this.on("load",this.iframeBind)}},update:function(t){if(this.descriptor.raw||(t=function(){}),"function"==typeof t){this.modifiers.stop&&(t=pe(t)),this.modifiers.prevent&&(t=de(t)),this.modifiers.self&&(t=ve(t));var e=Object.keys(this.modifiers).filter(function(t){return"stop"!==t&&"prevent"!==t&&"self"!==t&&"capture"!==t});e.length&&(t=fe(t,e)),this.reset(),this.handler=t,this.iframeBind?this.iframeBind():q(this.el,this.arg,this.handler,this.modifiers.capture)}},reset:function(){var t=this.iframeBind?this.el.contentWindow:this.el;this.handler&&Q(t,this.arg,this.handler)},unbind:function(){this.reset()}},rs=["-webkit-","-moz-","-ms-"],ss=["Webkit","Moz","ms"],os=/!important;?$/,as=Object.create(null),hs=null,ls={deep:!0,update:function(t){"string"==typeof t?this.el.style.cssText=t:Di(t)?this.handleObject(t.reduce(v,{})):this.handleObject(t||{})},handleObject:function(t){var e,i,n=this.cache||(this.cache={});for(e in n)e in t||(this.handleSingle(e,null),delete n[e]);for(e in t)i=t[e],i!==n[e]&&(n[e]=i,this.handleSingle(e,i))},handleSingle:function(t,e){if(t=me(t))if(null!=e&&(e+=""),e){var i=os.test(e)?"important":"";i?(e=e.replace(os,"").trim(),this.el.style.setProperty(t.kebab,e,i)):this.el.style[t.camel]=e}else this.el.style[t.camel]=""}},cs="http://www.w3.org/1999/xlink",us=/^xlink:/,fs=/^v-|^:|^@|^(?:is|transition|transition-mode|debounce|track-by|stagger|enter-stagger|leave-stagger)$/,ps=/^(?:value|checked|selected|muted)$/,ds=/^(?:draggable|contenteditable|spellcheck)$/,vs={value:"_value","true-value":"_trueValue","false-value":"_falseValue"},ms={priority:Hr,bind:function(){var t=this.arg,e=this.el.tagName;t||(this.deep=!0);var i=this.descriptor,n=i.interp;n&&(i.hasOneTime&&(this.expression=j(n,this._scope||this.vm)),(fs.test(t)||"name"===t&&("PARTIAL"===e||"SLOT"===e))&&(this.el.removeAttribute(t),this.invalid=!0))},update:function(t){
-if(!this.invalid){var e=this.arg;this.arg?this.handleSingle(e,t):this.handleObject(t||{})}},handleObject:ls.handleObject,handleSingle:function(t,e){var i=this.el,n=this.descriptor.interp;if(this.modifiers.camel&&(t=l(t)),!n&&ps.test(t)&&t in i){var r="value"===t&&null==e?"":e;i[t]!==r&&(i[t]=r)}var s=vs[t];if(!n&&s){i[s]=e;var o=i.__v_model;o&&o.listener()}return"value"===t&&"TEXTAREA"===i.tagName?void i.removeAttribute(t):void(ds.test(t)?i.setAttribute(t,e?"true":"false"):null!=e&&e!==!1?"class"===t?(i.__v_trans&&(e+=" "+i.__v_trans.id+"-transition"),Z(i,e)):us.test(t)?i.setAttributeNS(cs,t,e===!0?"":e):i.setAttribute(t,e===!0?"":e):i.removeAttribute(t))}},gs={priority:Mr,bind:function(){if(this.arg){var t=this.id=l(this.arg),e=(this._scope||this.vm).$els;i(e,t)?e[t]=this.el:kt(e,t,this.el)}},unbind:function(){var t=(this._scope||this.vm).$els;t[this.id]===this.el&&(t[this.id]=null)}},_s={bind:function(){}},ys={bind:function(){var t=this.el;this.vm.$once("pre-hook:compiled",function(){t.removeAttribute("v-cloak")})}},bs={text:kr,html:Dr,"for":qr,"if":Qr,show:Gr,model:es,on:ns,bind:ms,el:gs,ref:_s,cloak:ys},ws={deep:!0,update:function(t){t?"string"==typeof t?this.setClass(t.trim().split(/\s+/)):this.setClass(_e(t)):this.cleanup()},setClass:function(t){this.cleanup(t);for(var e=0,i=t.length;i>e;e++){var n=t[e];n&&ye(this.el,n,X)}this.prevKeys=t},cleanup:function(t){var e=this.prevKeys;if(e)for(var i=e.length;i--;){var n=e[i];(!t||t.indexOf(n)<0)&&ye(this.el,n,Y)}}},Cs={priority:Vr,params:["keep-alive","transition-mode","inline-template"],bind:function(){this.el.__vue__||(this.keepAlive=this.params.keepAlive,this.keepAlive&&(this.cache={}),this.params.inlineTemplate&&(this.inlineTemplate=K(this.el,!0)),this.pendingComponentCb=this.Component=null,this.pendingRemovals=0,this.pendingRemovalCb=null,this.anchor=nt("v-component"),J(this.el,this.anchor),this.el.removeAttribute("is"),this.el.removeAttribute(":is"),this.descriptor.ref&&this.el.removeAttribute("v-ref:"+u(this.descriptor.ref)),this.literal&&this.setComponent(this.expression))},update:function(t){this.literal||this.setComponent(t)},setComponent:function(t,e){if(this.invalidatePending(),t){var i=this;this.resolveComponent(t,function(){i.mountComponent(e)})}else this.unbuild(!0),this.remove(this.childVM,e),this.childVM=null},resolveComponent:function(t,e){var i=this;this.pendingComponentCb=w(function(n){i.ComponentName=n.options.name||("string"==typeof t?t:null),i.Component=n,e()}),this.vm._resolveComponent(t,this.pendingComponentCb)},mountComponent:function(t){this.unbuild(!0);var e=this,i=this.Component.options.activate,n=this.getCached(),r=this.build();i&&!n?(this.waitingFor=r,be(i,r,function(){e.waitingFor===r&&(e.waitingFor=null,e.transition(r,t))})):(n&&r._updateRef(),this.transition(r,t))},invalidatePending:function(){this.pendingComponentCb&&(this.pendingComponentCb.cancel(),this.pendingComponentCb=null)},build:function(t){var e=this.getCached();if(e)return e;if(this.Component){var i={name:this.ComponentName,el:Zt(this.el),template:this.inlineTemplate,parent:this._host||this.vm,_linkerCachable:!this.inlineTemplate,_ref:this.descriptor.ref,_asComponent:!0,_isRouterView:this._isRouterView,_context:this.vm,_scope:this._scope,_frag:this._frag};t&&v(i,t);var n=new this.Component(i);return this.keepAlive&&(this.cache[this.Component.cid]=n),n}},getCached:function(){return this.keepAlive&&this.cache[this.Component.cid]},unbuild:function(t){this.waitingFor&&(this.keepAlive||this.waitingFor.$destroy(),this.waitingFor=null);var e=this.childVM;return!e||this.keepAlive?void(e&&(e._inactive=!0,e._updateRef(!0))):void e.$destroy(!1,t)},remove:function(t,e){var i=this.keepAlive;if(t){this.pendingRemovals++,this.pendingRemovalCb=e;var n=this;t.$remove(function(){n.pendingRemovals--,i||t._cleanup(),!n.pendingRemovals&&n.pendingRemovalCb&&(n.pendingRemovalCb(),n.pendingRemovalCb=null)})}else e&&e()},transition:function(t,e){var i=this,n=this.childVM;switch(n&&(n._inactive=!0),t._inactive=!1,this.childVM=t,i.params.transitionMode){case"in-out":t.$before(i.anchor,function(){i.remove(n,e)});break;case"out-in":i.remove(n,function(){t.$before(i.anchor,e)});break;default:i.remove(n),t.$before(i.anchor,e)}},unbind:function(){if(this.invalidatePending(),this.unbuild(),this.cache){for(var t in this.cache)this.cache[t].$destroy();this.cache=null}}},$s=An._propBindingModes,ks={},xs=/^[$_a-zA-Z]+[\w$]*$/,As=An._propBindingModes,Os={bind:function(){var t=this.vm,e=t._context,i=this.descriptor.prop,n=i.path,r=i.parentPath,s=i.mode===As.TWO_WAY,o=this.parentWatcher=new Ut(e,r,function(e){xe(t,i,e)},{twoWay:s,filters:i.filters,scope:this._scope});if(ke(t,i,o.value),s){var a=this;t.$once("pre-hook:created",function(){a.childWatcher=new Ut(t,n,function(t){o.set(t)},{sync:!0})})}},unbind:function(){this.parentWatcher.teardown(),this.childWatcher&&this.childWatcher.teardown()}},Ts=[],Ns=!1,js="transition",Es="animation",Ss=Ji+"Duration",Fs=Qi+"Duration",Ds=Ri&&window.requestAnimationFrame,Ps=Ds?function(t){Ds(function(){Ds(t)})}:function(t){setTimeout(t,50)},Rs=Se.prototype;Rs.enter=function(t,e){this.cancelPending(),this.callHook("beforeEnter"),this.cb=e,X(this.el,this.enterClass),t(),this.entered=!1,this.callHookWithCb("enter"),this.entered||(this.cancel=this.hooks&&this.hooks.enterCancelled,je(this.enterNextTick))},Rs.enterNextTick=function(){var t=this;this.justEntered=!0,Ps(function(){t.justEntered=!1});var e=this.enterDone,i=this.getCssTransitionType(this.enterClass);this.pendingJsCb?i===js&&Y(this.el,this.enterClass):i===js?(Y(this.el,this.enterClass),this.setupCssCb(qi,e)):i===Es?this.setupCssCb(Gi,e):e()},Rs.enterDone=function(){this.entered=!0,this.cancel=this.pendingJsCb=null,Y(this.el,this.enterClass),this.callHook("afterEnter"),this.cb&&this.cb()},Rs.leave=function(t,e){this.cancelPending(),this.callHook("beforeLeave"),this.op=t,this.cb=e,X(this.el,this.leaveClass),this.left=!1,this.callHookWithCb("leave"),this.left||(this.cancel=this.hooks&&this.hooks.leaveCancelled,this.op&&!this.pendingJsCb&&(this.justEntered?this.leaveDone():je(this.leaveNextTick)))},Rs.leaveNextTick=function(){var t=this.getCssTransitionType(this.leaveClass);if(t){var e=t===js?qi:Gi;this.setupCssCb(e,this.leaveDone)}else this.leaveDone()},Rs.leaveDone=function(){this.left=!0,this.cancel=this.pendingJsCb=null,this.op(),Y(this.el,this.leaveClass),this.callHook("afterLeave"),this.cb&&this.cb(),this.op=null},Rs.cancelPending=function(){this.op=this.cb=null;var t=!1;this.pendingCssCb&&(t=!0,Q(this.el,this.pendingCssEvent,this.pendingCssCb),this.pendingCssEvent=this.pendingCssCb=null),this.pendingJsCb&&(t=!0,this.pendingJsCb.cancel(),this.pendingJsCb=null),t&&(Y(this.el,this.enterClass),Y(this.el,this.leaveClass)),this.cancel&&(this.cancel.call(this.vm,this.el),this.cancel=null)},Rs.callHook=function(t){this.hooks&&this.hooks[t]&&this.hooks[t].call(this.vm,this.el)},Rs.callHookWithCb=function(t){var e=this.hooks&&this.hooks[t];e&&(e.length>1&&(this.pendingJsCb=w(this[t+"Done"])),e.call(this.vm,this.el,this.pendingJsCb))},Rs.getCssTransitionType=function(t){if(!(!qi||document.hidden||this.hooks&&this.hooks.css===!1||Fe(this.el))){var e=this.type||this.typeCache[t];if(e)return e;var i=this.el.style,n=window.getComputedStyle(this.el),r=i[Ss]||n[Ss];if(r&&"0s"!==r)e=js;else{var s=i[Fs]||n[Fs];s&&"0s"!==s&&(e=Es)}return e&&(this.typeCache[t]=e),e}},Rs.setupCssCb=function(t,e){this.pendingCssEvent=t;var i=this,n=this.el,r=this.pendingCssCb=function(s){s.target===n&&(Q(n,t,r),i.pendingCssEvent=i.pendingCssCb=null,!i.pendingJsCb&&e&&e())};q(n,t,r)};var Ls={priority:Ir,update:function(t,e){var i=this.el,n=gt(this.vm.$options,"transitions",t);t=t||"v",e=e||"v",i.__v_trans=new Se(i,t,n,this.vm),Y(i,e+"-transition"),X(i,t+"-transition")}},Hs={style:ls,"class":ws,component:Cs,prop:Os,transition:Ls},Is=/^v-bind:|^:/,Ms=/^v-on:|^@/,Vs=/^v-([^:]+)(?:$|:(.*)$)/,Bs=/\.[^\.]+/g,Ws=/^(v-bind:|:)?transition$/,zs=1e3,Us=2e3;Ye.terminal=!0;var Js=/[^\w\-:\.]/,qs=Object.freeze({compile:De,compileAndLinkProps:Ie,compileRoot:Me,transclude:si,resolveSlots:li}),Qs=/^v-on:|^@/;di.prototype._bind=function(){var t=this.name,e=this.descriptor;if(("cloak"!==t||this.vm._isCompiled)&&this.el&&this.el.removeAttribute){var i=e.attr||"v-"+t;this.el.removeAttribute(i)}var n=e.def;if("function"==typeof n?this.update=n:v(this,n),this._setupParams(),this.bind&&this.bind(),this._bound=!0,this.literal)this.update&&this.update(e.raw);else if((this.expression||this.modifiers)&&(this.update||this.twoWay)&&!this._checkStatement()){var r=this;this.update?this._update=function(t,e){r._locked||r.update(t,e)}:this._update=pi;var s=this._preProcess?p(this._preProcess,this):null,o=this._postProcess?p(this._postProcess,this):null,a=this._watcher=new Ut(this.vm,this.expression,this._update,{filters:this.filters,twoWay:this.twoWay,deep:this.deep,preProcess:s,postProcess:o,scope:this._scope});this.afterBind?this.afterBind():this.update&&this.update(a.value)}},di.prototype._setupParams=function(){if(this.params){var t=this.params;this.params=Object.create(null);for(var e,i,n,r=t.length;r--;)e=u(t[r]),n=l(e),i=M(this.el,e),null!=i?this._setupParamWatcher(n,i):(i=I(this.el,e),null!=i&&(this.params[n]=""===i?!0:i))}},di.prototype._setupParamWatcher=function(t,e){var i=this,n=!1,r=(this._scope||this.vm).$watch(e,function(e,r){if(i.params[t]=e,n){var s=i.paramWatchers&&i.paramWatchers[t];s&&s.call(i,e,r)}else n=!0},{immediate:!0,user:!1});(this._paramUnwatchFns||(this._paramUnwatchFns=[])).push(r)},di.prototype._checkStatement=function(){var t=this.expression;if(t&&this.acceptStatement&&!Mt(t)){var e=It(t).get,i=this._scope||this.vm,n=function(t){i.$event=t,e.call(i,i),i.$event=null};return this.filters&&(n=i._applyFilters(n,null,this.filters)),this.update(n),!0}},di.prototype.set=function(t){this.twoWay&&this._withLock(function(){this._watcher.set(t)})},di.prototype._withLock=function(t){var e=this;e._locked=!0,t.call(e),Yi(function(){e._locked=!1})},di.prototype.on=function(t,e,i){q(this.el,t,e,i),(this._listeners||(this._listeners=[])).push([t,e])},di.prototype._teardown=function(){if(this._bound){this._bound=!1,this.unbind&&this.unbind(),this._watcher&&this._watcher.teardown();var t,e=this._listeners;if(e)for(t=e.length;t--;)Q(this.el,e[t][0],e[t][1]);var i=this._paramUnwatchFns;if(i)for(t=i.length;t--;)i[t]();this.vm=this.el=this._watcher=this._listeners=null}};var Gs=/[^|]\|[^|]/;xt(wi),ui(wi),fi(wi),vi(wi),mi(wi),gi(wi),_i(wi),yi(wi),bi(wi);var Zs={priority:Ur,params:["name"],bind:function(){var t=this.params.name||"default",e=this.vm._slotContents&&this.vm._slotContents[t];e&&e.hasChildNodes()?this.compile(e.cloneNode(!0),this.vm._context,this.vm):this.fallback()},compile:function(t,e,i){if(t&&e){if(this.el.hasChildNodes()&&1===t.childNodes.length&&1===t.childNodes[0].nodeType&&t.childNodes[0].hasAttribute("v-if")){var n=document.createElement("template");n.setAttribute("v-else",""),n.innerHTML=this.el.innerHTML,n._context=this.vm,t.appendChild(n)}var r=i?i._scope:this._scope;this.unlink=e.$compile(t,i,r,this._frag)}t?J(this.el,t):z(this.el)},fallback:function(){this.compile(K(this.el,!0),this.vm)},unbind:function(){this.unlink&&this.unlink()}},Xs={priority:Br,params:["name"],paramWatchers:{name:function(t){Qr.remove.call(this),t&&this.insert(t)}},bind:function(){this.anchor=nt("v-partial"),J(this.el,this.anchor),this.insert(this.params.name)},insert:function(t){var e=gt(this.vm.$options,"partials",t,!0);e&&(this.factory=new se(this.vm,e),Qr.insert.call(this))},unbind:function(){this.frag&&this.frag.destroy()}},Ys={slot:Zs,partial:Xs},Ks=qr._postProcess,to=/(\d{3})(?=\d)/g,eo={orderBy:ki,filterBy:$i,limitBy:Ci,json:{read:function(t,e){return"string"==typeof t?t:JSON.stringify(t,null,arguments.length>1?e:2)},write:function(t){try{return JSON.parse(t)}catch(e){return t}}},capitalize:function(t){return t||0===t?(t=t.toString(),t.charAt(0).toUpperCase()+t.slice(1)):""},uppercase:function(t){return t||0===t?t.toString().toUpperCase():""},lowercase:function(t){return t||0===t?t.toString().toLowerCase():""},currency:function(t,e,i){if(t=parseFloat(t),!isFinite(t)||!t&&0!==t)return"";e=null!=e?e:"$",i=null!=i?i:2;var n=Math.abs(t).toFixed(i),r=i?n.slice(0,-1-i):n,s=r.length%3,o=s>0?r.slice(0,s)+(r.length>3?",":""):"",a=i?n.slice(-1-i):"",h=0>t?"-":"";return h+e+o+r.slice(s).replace(to,"$1,")+a},pluralize:function(t){var e=d(arguments,1),i=e.length;if(i>1){var n=t%10-1;return n in e?e[n]:e[i-1]}return e[0]+(1===t?"":"s")},debounce:function(t,e){return t?(e||(e=300),y(t,e)):void 0}};return Ai(wi),wi.version="1.0.26",setTimeout(function(){An.devtools&&Li&&Li.emit("init",wi)},0),wi});
-//# sourceMappingURL=vue.min.js.map \ No newline at end of file
+!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function t(e){var t=parseFloat(e,10);return t||0===t?t:e}function n(e,t){for(var n=Object.create(null),r=e.split(","),i=0;i<r.length;i++)n[r[i]]=!0;return t?function(e){return n[e.toLowerCase()]}:function(e){return n[e]}}function r(e,t){if(e.length){var n=e.indexOf(t);if(n>-1)return e.splice(n,1)}}function i(e,t){return _r.call(e,t)}function a(e){return"string"==typeof e||"number"==typeof e}function o(e){var t=Object.create(null);return function(n){var r=t[n];return r||(t[n]=e(n))}}function s(e,t){function n(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}return n._length=e.length,n}function c(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function u(e,t){for(var n in t)e[n]=t[n];return e}function l(e){return null!==e&&"object"==typeof e}function f(e){return kr.call(e)===Ar}function d(e){for(var t={},n=0;n<e.length;n++)e[n]&&u(t,e[n]);return t}function p(){}function v(e){return e.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(",")}function h(e,t){return e==t||!(!l(e)||!l(t))&&JSON.stringify(e)===JSON.stringify(t)}function m(e,t){for(var n=0;n<e.length;n++)if(h(e[n],t))return n;return-1}function g(e){var t=(e+"").charCodeAt(0);return 36===t||95===t}function y(e,t,n,r){Object.defineProperty(e,t,{value:n,enumerable:!!r,writable:!0,configurable:!0})}function _(e){if(!Sr.test(e)){var t=e.split(".");return function(e){for(var n=0;n<t.length;n++){if(!e)return;e=e[t[n]]}return e}}}function b(e){return/native code/.test(e.toString())}function $(e){Hr.target&&Ur.push(Hr.target),Hr.target=e}function w(){Hr.target=Ur.pop()}function C(){zr.length=0,Vr={},Jr=qr=!1}function x(){for(qr=!0,zr.sort(function(e,t){return e.id-t.id}),Kr=0;Kr<zr.length;Kr++){var e=zr[Kr],t=e.id;Vr[t]=null,e.run()}Ir&&Tr.devtools&&Ir.emit("flush"),C()}function k(e){var t=e.id;if(null==Vr[t]){if(Vr[t]=!0,qr){for(var n=zr.length-1;n>=0&&zr[n].id>e.id;)n--;zr.splice(Math.max(n,Kr)+1,0,e)}else zr.push(e);Jr||(Jr=!0,Br(x))}}function A(e,t){var n,r;t||(t=Gr,t.clear());var i=Array.isArray(e),a=l(e);if((i||a)&&Object.isExtensible(e)){if(e.__ob__){var o=e.__ob__.dep.id;if(t.has(o))return;t.add(o)}if(i)for(n=e.length;n--;)A(e[n],t);else if(a)for(r=Object.keys(e),n=r.length;n--;)A(e[r[n]],t)}}function O(e,t){e.__proto__=t}function T(e,t,n){for(var r=0,i=n.length;r<i;r++){var a=n[r];y(e,a,t[a])}}function S(e){if(l(e)){var t;return i(e,"__ob__")&&e.__ob__ instanceof ti?t=e.__ob__:ei.shouldConvert&&!Tr._isServer&&(Array.isArray(e)||f(e))&&Object.isExtensible(e)&&!e._isVue&&(t=new ti(e)),t}}function E(e,t,n,r){var i=new Hr,a=Object.getOwnPropertyDescriptor(e,t);if(!a||a.configurable!==!1){var o=a&&a.get,s=a&&a.set,c=S(n);Object.defineProperty(e,t,{enumerable:!0,configurable:!0,get:function(){var t=o?o.call(e):n;return Hr.target&&(i.depend(),c&&c.dep.depend(),Array.isArray(t)&&N(t)),t},set:function(t){var r=o?o.call(e):n;t!==r&&(s?s.call(e,t):n=t,c=S(t),i.notify())}})}}function j(e,t,n){if(Array.isArray(e))return e.splice(t,1,n),n;if(i(e,t))return void(e[t]=n);var r=e.__ob__;if(!(e._isVue||r&&r.vmCount))return r?(E(r.value,t,n),r.dep.notify(),n):void(e[t]=n)}function L(e,t){var n=e.__ob__;e._isVue||n&&n.vmCount||i(e,t)&&(delete e[t],n&&n.dep.notify())}function N(e){for(var t=void 0,n=0,r=e.length;n<r;n++)t=e[n],t&&t.__ob__&&t.__ob__.dep.depend(),Array.isArray(t)&&N(t)}function D(e){e._watchers=[],M(e),P(e),R(e),B(e),F(e)}function M(e){var t=e.$options.props;if(t){var n=e.$options.propsData||{},r=e.$options._propKeys=Object.keys(t),i=!e.$parent;ei.shouldConvert=i;for(var a=function(i){var a=r[i];E(e,a,Le(a,t,n,e))},o=0;o<r.length;o++)a(o);ei.shouldConvert=!0}}function P(e){var t=e.$options.data;t=e._data="function"==typeof t?t.call(e):t||{},f(t)||(t={});for(var n=Object.keys(t),r=e.$options.props,a=n.length;a--;)r&&i(r,n[a])||z(e,n[a]);S(t),t.__ob__&&t.__ob__.vmCount++}function R(e){var t=e.$options.computed;if(t)for(var n in t){var r=t[n];"function"==typeof r?(ni.get=I(r,e),ni.set=p):(ni.get=r.get?r.cache!==!1?I(r.get,e):s(r.get,e):p,ni.set=r.set?s(r.set,e):p),Object.defineProperty(e,n,ni)}}function I(e,t){var n=new Zr(t,e,p,{lazy:!0});return function(){return n.dirty&&n.evaluate(),Hr.target&&n.depend(),n.value}}function B(e){var t=e.$options.methods;if(t)for(var n in t)e[n]=null==t[n]?p:s(t[n],e)}function F(e){var t=e.$options.watch;if(t)for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;i<r.length;i++)H(e,n,r[i]);else H(e,n,r)}}function H(e,t,n){var r;f(n)&&(r=n,n=n.handler),"string"==typeof n&&(n=e[n]),e.$watch(t,n,r)}function U(e){var t={};t.get=function(){return this._data},Object.defineProperty(e.prototype,"$data",t),e.prototype.$set=j,e.prototype.$delete=L,e.prototype.$watch=function(e,t,n){var r=this;n=n||{},n.user=!0;var i=new Zr(r,e,t,n);return n.immediate&&t.call(r,i.value),function(){i.teardown()}}}function z(e,t){g(t)||Object.defineProperty(e,t,{configurable:!0,enumerable:!0,get:function(){return e._data[t]},set:function(n){e._data[t]=n}})}function V(e){var t=new ri(e.tag,e.data,e.children,e.text,e.elm,e.ns,e.context,e.componentOptions);return t.isStatic=e.isStatic,t.key=e.key,t.isCloned=!0,t}function J(e){for(var t=new Array(e.length),n=0;n<e.length;n++)t[n]=V(e[n]);return t}function q(e,t,n,r){r+=t;var i=e.__injected||(e.__injected={});if(!i[r]){i[r]=!0;var a=e[t];a?e[t]=function(){a.apply(this,arguments),n.apply(this,arguments)}:e[t]=n}}function K(e,t,n,r,i){var a,o,s,c,u,l;for(a in e)if(o=e[a],s=t[a],o)if(s){if(o!==s)if(Array.isArray(s)){s.length=o.length;for(var f=0;f<s.length;f++)s[f]=o[f];e[a]=s}else s.fn=o,e[a]=s}else l="!"===a.charAt(0),u=l?a.slice(1):a,Array.isArray(o)?n(u,o.invoker=W(o),l):(o.invoker||(c=o,o=e[a]={},o.fn=c,o.invoker=Z(o)),n(u,o.invoker,l));else;for(a in t)e[a]||(u="!"===a.charAt(0)?a.slice(1):a,r(u,t[a].invoker))}function W(e){return function(t){for(var n=arguments,r=1===arguments.length,i=0;i<e.length;i++)r?e[i](t):e[i].apply(null,n)}}function Z(e){return function(t){var n=1===arguments.length;n?e.fn(t):e.fn.apply(null,arguments)}}function G(e,t,n){if(a(e))return[Y(e)];if(Array.isArray(e)){for(var r=[],i=0,o=e.length;i<o;i++){var s=e[i],c=r[r.length-1];Array.isArray(s)?r.push.apply(r,G(s,t,(n||"")+"_"+i)):a(s)?c&&c.text?c.text+=String(s):""!==s&&r.push(Y(s)):s instanceof ri&&(s.text&&c&&c.text?c.text+=s.text:(t&&Q(s,t),s.tag&&null==s.key&&null!=n&&(s.key="__vlist"+n+"_"+i+"__"),r.push(s)))}return r}}function Y(e){return new ri(void 0,void 0,void 0,String(e))}function Q(e,t){if(e.tag&&!e.ns&&(e.ns=t,e.children))for(var n=0,r=e.children.length;n<r;n++)Q(e.children[n],t)}function X(e){return e&&e.filter(function(e){return e&&e.componentOptions})[0]}function ee(e){var t=e.$options,n=t.parent;if(n&&!t.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(e)}e.$parent=n,e.$root=n?n.$root:e,e.$children=[],e.$refs={},e._watcher=null,e._inactive=!1,e._isMounted=!1,e._isDestroyed=!1,e._isBeingDestroyed=!1}function te(e){e.prototype._mount=function(e,t){var n=this;return n.$el=e,n.$options.render||(n.$options.render=ii),ne(n,"beforeMount"),n._watcher=new Zr(n,function(){n._update(n._render(),t)},p),t=!1,null==n.$vnode&&(n._isMounted=!0,ne(n,"mounted")),n},e.prototype._update=function(e,t){var n=this;n._isMounted&&ne(n,"beforeUpdate");var r=n.$el,i=ai;ai=n;var a=n._vnode;n._vnode=e,a?n.$el=n.__patch__(a,e):n.$el=n.__patch__(n.$el,e,t),ai=i,r&&(r.__vue__=null),n.$el&&(n.$el.__vue__=n),n.$vnode&&n.$parent&&n.$vnode===n.$parent._vnode&&(n.$parent.$el=n.$el),n._isMounted&&ne(n,"updated")},e.prototype._updateFromParent=function(e,t,n,r){var i=this,a=!(!i.$options._renderChildren&&!r);if(i.$options._parentVnode=n,i.$options._renderChildren=r,e&&i.$options.props){ei.shouldConvert=!1;for(var o=i.$options._propKeys||[],s=0;s<o.length;s++){var c=o[s];i[c]=Le(c,i.$options.props,e,i)}ei.shouldConvert=!0}if(t){var u=i.$options._parentListeners;i.$options._parentListeners=t,i._updateListeners(t,u)}a&&(i.$slots=_e(r,i._renderContext),i.$forceUpdate())},e.prototype.$forceUpdate=function(){var e=this;e._watcher&&e._watcher.update()},e.prototype.$destroy=function(){var e=this;if(!e._isBeingDestroyed){ne(e,"beforeDestroy"),e._isBeingDestroyed=!0;var t=e.$parent;!t||t._isBeingDestroyed||e.$options.abstract||r(t.$children,e),e._watcher&&e._watcher.teardown();for(var n=e._watchers.length;n--;)e._watchers[n].teardown();e._data.__ob__&&e._data.__ob__.vmCount--,e._isDestroyed=!0,ne(e,"destroyed"),e.$off(),e.$el&&(e.$el.__vue__=null),e.__patch__(e._vnode,null)}}}function ne(e,t){var n=e.$options[t];if(n)for(var r=0,i=n.length;r<i;r++)n[r].call(e);e.$emit("hook:"+t)}function re(e,t,n,r,i){if(e&&(l(e)&&(e=Ce.extend(e)),"function"==typeof e)){if(!e.cid)if(e.resolved)e=e.resolved;else if(e=le(e,function(){n.$forceUpdate()}),!e)return;t=t||{};var a=fe(t,e);if(e.options.functional)return ie(e,a,t,n,r);var o=t.on;t.on=t.nativeOn,e.options.abstract&&(t={}),pe(t);var s=e.options.name||i,c=new ri("vue-component-"+e.cid+(s?"-"+s:""),t,void 0,void 0,void 0,void 0,n,{Ctor:e,propsData:a,listeners:o,tag:i,children:r});return c}}function ie(e,t,n,r,i){var a={},o=e.options.props;if(o)for(var c in o)a[c]=Le(c,o,t);var u=e.options.render.call(null,s(he,{_self:Object.create(r)}),{props:a,data:n,parent:r,children:G(i),slots:function(){return _e(i,r)}});return u instanceof ri&&(u.functionalContext=r,n.slot&&((u.data||(u.data={})).slot=n.slot)),u}function ae(e,t){var n=e.componentOptions,r={_isComponent:!0,parent:t,propsData:n.propsData,_componentTag:n.tag,_parentVnode:e,_parentListeners:n.listeners,_renderChildren:n.children},i=e.data.inlineTemplate;return i&&(r.render=i.render,r.staticRenderFns=i.staticRenderFns),new n.Ctor(r)}function oe(e,t){if(!e.child||e.child._isDestroyed){var n=e.child=ae(e,ai);n.$mount(t?e.elm:void 0,t)}}function se(e,t){var n=t.componentOptions,r=t.child=e.child;r._updateFromParent(n.propsData,n.listeners,t,n.children)}function ce(e){e.child._isMounted||(e.child._isMounted=!0,ne(e.child,"mounted")),e.data.keepAlive&&(e.child._inactive=!1,ne(e.child,"activated"))}function ue(e){e.child._isDestroyed||(e.data.keepAlive?(e.child._inactive=!0,ne(e.child,"deactivated")):e.child.$destroy())}function le(e,t){if(!e.requested){e.requested=!0;var n=e.pendingCallbacks=[t],r=!0,i=function(t){if(l(t)&&(t=Ce.extend(t)),e.resolved=t,!r)for(var i=0,a=n.length;i<a;i++)n[i](t)},a=function(e){},o=e(i,a);return o&&"function"==typeof o.then&&!e.resolved&&o.then(i,a),r=!1,e.resolved}e.pendingCallbacks.push(t)}function fe(e,t){var n=t.options.props;if(n){var r={},i=e.attrs,a=e.props,o=e.domProps;if(i||a||o)for(var s in n){var c=xr(s);de(r,a,s,c,!0)||de(r,i,s,c)||de(r,o,s,c)}return r}}function de(e,t,n,r,a){if(t){if(i(t,n))return e[n]=t[n],a||delete t[n],!0;if(i(t,r))return e[n]=t[r],a||delete t[r],!0}return!1}function pe(e){e.hook||(e.hook={});for(var t=0;t<si.length;t++){var n=si[t],r=e.hook[n],i=oi[n];e.hook[n]=r?ve(i,r):i}}function ve(e,t){return function(n,r){e(n,r),t(n,r)}}function he(e,t,n){return t&&(Array.isArray(t)||"object"!=typeof t)&&(n=t,t=void 0),me(this._self,e,t,n)}function me(e,t,n,r){if(!n||!n.__ob__){if(!t)return ii();if("string"==typeof t){var i,a=Tr.getTagNamespace(t);return Tr.isReservedTag(t)?new ri(t,n,G(r,a),void 0,void 0,a,e):(i=je(e.$options,"components",t))?re(i,n,e,r,t):new ri(t,n,G(r,a),void 0,void 0,a,e)}return re(t,n,e,r)}}function ge(e){e.$vnode=null,e._vnode=null,e._staticTrees=null,e._renderContext=e.$options._parentVnode&&e.$options._parentVnode.context,e.$slots=_e(e.$options._renderChildren,e._renderContext),e.$createElement=s(he,e),e.$options.el&&e.$mount(e.$options.el)}function ye(n){n.prototype.$nextTick=function(e){Br(e,this)},n.prototype._render=function(){var e=this,t=e.$options,n=t.render,r=t.staticRenderFns,i=t._parentVnode;if(e._isMounted)for(var a in e.$slots)e.$slots[a]=J(e.$slots[a]);r&&!e._staticTrees&&(e._staticTrees=[]),e.$vnode=i;var o;try{o=n.call(e._renderProxy,e.$createElement)}catch(t){if(Tr.errorHandler)Tr.errorHandler.call(null,t,e);else{if(Tr._isServer)throw t;setTimeout(function(){throw t},0)}o=e._vnode}return o instanceof ri||(o=ii()),o.parent=i,o},n.prototype._h=he,n.prototype._s=e,n.prototype._n=t,n.prototype._e=ii,n.prototype._q=h,n.prototype._i=m,n.prototype._m=function(e,t){var n=this._staticTrees[e];if(n&&!t)return Array.isArray(n)?J(n):V(n);if(n=this._staticTrees[e]=this.$options.staticRenderFns[e].call(this._renderProxy),Array.isArray(n))for(var r=0;r<n.length;r++)"string"!=typeof n[r]&&(n[r].isStatic=!0,n[r].key="__static__"+e+"_"+r);else n.isStatic=!0,n.key="__static__"+e;return n};var r=function(e){return e};n.prototype._f=function(e){return je(this.$options,"filters",e,!0)||r},n.prototype._l=function(e,t){var n,r,i,a,o;if(Array.isArray(e))for(n=new Array(e.length),r=0,i=e.length;r<i;r++)n[r]=t(e[r],r);else if("number"==typeof e)for(n=new Array(e),r=0;r<e;r++)n[r]=t(r+1,r);else if(l(e))for(a=Object.keys(e),n=new Array(a.length),r=0,i=a.length;r<i;r++)o=a[r],n[r]=t(e[o],o,r);return n},n.prototype._t=function(e,t){var n=this.$slots[e];return n||t},n.prototype._b=function(e,t,n){if(t)if(l(t)){Array.isArray(t)&&(t=d(t));for(var r in t)if("class"===r||"style"===r)e[r]=t[r];else{var i=n||Tr.mustUseProp(r)?e.domProps||(e.domProps={}):e.attrs||(e.attrs={});i[r]=t[r]}}else;return e},n.prototype._k=function(e){return Tr.keyCodes[e]}}function _e(e,t){var n={};if(!e)return n;for(var r,i,a=G(e)||[],o=[],s=0,c=a.length;s<c;s++)if(i=a[s],(i.context===t||i.functionalContext===t)&&i.data&&(r=i.data.slot)){var u=n[r]||(n[r]=[]);"template"===i.tag?u.push.apply(u,i.children):u.push(i)}else o.push(i);return o.length&&(1!==o.length||" "!==o[0].text&&!o[0].isComment)&&(n.default=o),n}function be(e){e._events=Object.create(null);var t=e.$options._parentListeners,n=s(e.$on,e),r=s(e.$off,e);e._updateListeners=function(t,i){K(t,i||{},n,r,e)},t&&e._updateListeners(t)}function $e(e){e.prototype.$on=function(e,t){var n=this;return(n._events[e]||(n._events[e]=[])).push(t),n},e.prototype.$once=function(e,t){function n(){r.$off(e,n),t.apply(r,arguments)}var r=this;return n.fn=t,r.$on(e,n),r},e.prototype.$off=function(e,t){var n=this;if(!arguments.length)return n._events=Object.create(null),n;var r=n._events[e];if(!r)return n;if(1===arguments.length)return n._events[e]=null,n;for(var i,a=r.length;a--;)if(i=r[a],i===t||i.fn===t){r.splice(a,1);break}return n},e.prototype.$emit=function(e){var t=this,n=t._events[e];if(n){n=n.length>1?c(n):n;for(var r=c(arguments,1),i=0,a=n.length;i<a;i++)n[i].apply(t,r)}return t}}function we(e){function t(e,t){var r=e.$options=Object.create(n(e));r.parent=t.parent,r.propsData=t.propsData,r._parentVnode=t._parentVnode,r._parentListeners=t._parentListeners,r._renderChildren=t._renderChildren,r._componentTag=t._componentTag,t.render&&(r.render=t.render,r.staticRenderFns=t.staticRenderFns)}function n(e){var t=e.constructor,n=t.options;if(t.super){var r=t.super.options,i=t.superOptions;r!==i&&(t.superOptions=r,n=t.options=Ee(r,t.extendOptions),n.name&&(n.components[n.name]=t))}return n}e.prototype._init=function(e){var r=this;r._uid=ci++,r._isVue=!0,e&&e._isComponent?t(r,e):r.$options=Ee(n(r),e||{},r),r._renderProxy=r,r._self=r,ee(r),be(r),ne(r,"beforeCreate"),D(r),ne(r,"created"),ge(r)}}function Ce(e){this._init(e)}function xe(e,t){var n,r,a;for(n in t)r=e[n],a=t[n],i(e,n)?l(r)&&l(a)&&xe(r,a):j(e,n,a);return e}function ke(e,t){return t?e?e.concat(t):Array.isArray(t)?t:[t]:e}function Ae(e,t){var n=Object.create(e||null);return t?u(n,t):n}function Oe(e){if(e.components){var t,n=e.components;for(var r in n){var i=r.toLowerCase();yr(i)||Tr.isReservedTag(i)||(t=n[r],f(t)&&(n[r]=Ce.extend(t)))}}}function Te(e){var t=e.props;if(t){var n,r,i,a={};if(Array.isArray(t))for(n=t.length;n--;)r=t[n],"string"==typeof r&&(i=$r(r),a[i]={type:null});else if(f(t))for(var o in t)r=t[o],i=$r(o),a[i]=f(r)?r:{type:r};e.props=a}}function Se(e){var t=e.directives;if(t)for(var n in t){var r=t[n];"function"==typeof r&&(t[n]={bind:r,update:r})}}function Ee(e,t,n){function r(r){var i=fi[r]||di;l[r]=i(e[r],t[r],n,r)}Oe(t),Te(t),Se(t);var a=t.extends;if(a&&(e="function"==typeof a?Ee(e,a.options,n):Ee(e,a,n)),t.mixins)for(var o=0,s=t.mixins.length;o<s;o++){var c=t.mixins[o];c.prototype instanceof Ce&&(c=c.options),e=Ee(e,c,n)}var u,l={};for(u in e)r(u);for(u in t)i(e,u)||r(u);return l}function je(e,t,n,r){if("string"==typeof n){var i=e[t],a=i[n]||i[$r(n)]||i[wr($r(n))];return a}}function Le(e,t,n,r){var a=t[e],o=!i(n,e),s=n[e];if(Me(a.type)&&(o&&!i(a,"default")?s=!1:""!==s&&s!==xr(e)||(s=!0)),void 0===s){s=Ne(r,a,e);var c=ei.shouldConvert;ei.shouldConvert=!0,S(s),ei.shouldConvert=c}return s}function Ne(e,t,n){if(i(t,"default")){var r=t.default;return l(r),"function"==typeof r&&t.type!==Function?r.call(e):r}}function De(e){var t=e&&e.toString().match(/^\s*function (\w+)/);return t&&t[1]}function Me(e){if(!Array.isArray(e))return"Boolean"===De(e);for(var t=0,n=e.length;t<n;t++)if("Boolean"===De(e[t]))return!0;return!1}function Pe(e){e.use=function(e){if(!e.installed){var t=c(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):e.apply(null,t),e.installed=!0,this}}}function Re(e){e.mixin=function(t){e.options=Ee(e.options,t)}}function Ie(e){e.cid=0;var t=1;e.extend=function(e){e=e||{};var n=this,r=0===n.cid;if(r&&e._Ctor)return e._Ctor;var i=e.name||n.options.name,a=function(e){this._init(e)};return a.prototype=Object.create(n.prototype),a.prototype.constructor=a,a.cid=t++,a.options=Ee(n.options,e),a.super=n,a.extend=n.extend,Tr._assetTypes.forEach(function(e){a[e]=n[e]}),i&&(a.options.components[i]=a),a.superOptions=n.options,a.extendOptions=e,r&&(e._Ctor=a),a}}function Be(e){Tr._assetTypes.forEach(function(t){e[t]=function(n,r){return r?("component"===t&&f(r)&&(r.name=r.name||n,r=e.extend(r)),"directive"===t&&"function"==typeof r&&(r={bind:r,update:r}),this.options[t+"s"][n]=r,r):this.options[t+"s"][n]}})}function Fe(e){var t={};t.get=function(){return Tr},Object.defineProperty(e,"config",t),e.util=pi,e.set=j,e.delete=L,e.nextTick=Br,e.options=Object.create(null),Tr._assetTypes.forEach(function(t){e.options[t+"s"]=Object.create(null)}),u(e.options.components,hi),Pe(e),Re(e),Ie(e),Be(e)}function He(e){for(var t=e.data,n=e,r=e;r.child;)r=r.child._vnode,r.data&&(t=Ue(r.data,t));for(;n=n.parent;)n.data&&(t=Ue(t,n.data));return ze(t)}function Ue(e,t){return{staticClass:Ve(e.staticClass,t.staticClass),class:e.class?[e.class,t.class]:t.class}}function ze(e){var t=e.class,n=e.staticClass;return n||t?Ve(n,Je(t)):""}function Ve(e,t){return e?t?e+" "+t:e:t||""}function Je(e){var t="";if(!e)return t;if("string"==typeof e)return e;if(Array.isArray(e)){for(var n,r=0,i=e.length;r<i;r++)e[r]&&(n=Je(e[r]))&&(t+=n+" ");return t.slice(0,-1)}if(l(e)){for(var a in e)e[a]&&(t+=a+" ");return t.slice(0,-1)}return t}function qe(e){return Si(e)?"svg":"math"===e?"math":void 0}function Ke(e){if(!jr)return!0;if(ji(e))return!1;if(e=e.toLowerCase(),null!=Li[e])return Li[e];var t=document.createElement(e);return e.indexOf("-")>-1?Li[e]=t.constructor===window.HTMLUnknownElement||t.constructor===window.HTMLElement:Li[e]=/HTMLUnknownElement/.test(t.toString())}function We(e){if("string"==typeof e){if(e=document.querySelector(e),!e)return document.createElement("div")}return e}function Ze(e,t){var n=document.createElement(e);return"select"!==e?n:(t.data&&t.data.attrs&&"multiple"in t.data.attrs&&n.setAttribute("multiple","multiple"),n)}function Ge(e,t){return document.createElementNS(xi[e],t)}function Ye(e){return document.createTextNode(e)}function Qe(e){return document.createComment(e)}function Xe(e,t,n){e.insertBefore(t,n)}function et(e,t){e.removeChild(t)}function tt(e,t){e.appendChild(t)}function nt(e){return e.parentNode}function rt(e){return e.nextSibling}function it(e){return e.tagName}function at(e,t){e.textContent=t}function ot(e){return e.childNodes}function st(e,t,n){e.setAttribute(t,n)}function ct(e,t){var n=e.data.ref;if(n){var i=e.context,a=e.child||e.elm,o=i.$refs;t?Array.isArray(o[n])?r(o[n],a):o[n]===a&&(o[n]=void 0):e.data.refInFor?Array.isArray(o[n])?o[n].push(a):o[n]=[a]:o[n]=a}}function ut(e){return null==e}function lt(e){return null!=e}function ft(e,t){return e.key===t.key&&e.tag===t.tag&&e.isComment===t.isComment&&!e.data==!t.data}function dt(e,t,n){var r,i,a={};for(r=t;r<=n;++r)i=e[r].key,lt(i)&&(a[i]=r);return a}function pt(e){function t(e){return new ri(C.tagName(e).toLowerCase(),{},[],void 0,e)}function n(e,t){function n(){0===--n.listeners&&r(e)}return n.listeners=t,n}function r(e){var t=C.parentNode(e);C.removeChild(t,e)}function i(e,t,n){var r,i=e.data;if(e.isRootInsert=!n,lt(i)&&(lt(r=i.hook)&&lt(r=r.init)&&r(e),lt(r=e.child)))return u(e,t),e.elm;var a=e.children,s=e.tag;return lt(s)?(e.elm=e.ns?C.createElementNS(e.ns,s):C.createElement(s,e),l(e),o(e,a,t),lt(i)&&c(e,t)):e.isComment?e.elm=C.createComment(e.text):e.elm=C.createTextNode(e.text),e.elm}function o(e,t,n){if(Array.isArray(t))for(var r=0;r<t.length;++r)C.appendChild(e.elm,i(t[r],n,!0));else a(e.text)&&C.appendChild(e.elm,C.createTextNode(e.text))}function s(e){for(;e.child;)e=e.child._vnode;return lt(e.tag)}function c(e,t){for(var n=0;n<$.create.length;++n)$.create[n](Mi,e);_=e.data.hook,lt(_)&&(_.create&&_.create(Mi,e),_.insert&&t.push(e))}function u(e,t){e.data.pendingInsert&&t.push.apply(t,e.data.pendingInsert),e.elm=e.child.$el,s(e)?(c(e,t),l(e)):(ct(e),t.push(e))}function l(e){var t;lt(t=e.context)&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,""),lt(t=ai)&&t!==e.context&&lt(t=t.$options._scopeId)&&C.setAttribute(e.elm,t,"")}function f(e,t,n,r,a,o){for(;r<=a;++r)C.insertBefore(e,i(n[r],o),t)}function d(e){var t,n,r=e.data;if(lt(r))for(lt(t=r.hook)&&lt(t=t.destroy)&&t(e),t=0;t<$.destroy.length;++t)$.destroy[t](e);if(lt(t=e.children))for(n=0;n<e.children.length;++n)d(e.children[n])}function p(e,t,n,r){for(;n<=r;++n){var i=t[n];lt(i)&&(lt(i.tag)?(v(i),d(i)):C.removeChild(e,i.elm))}}function v(e,t){if(t||lt(e.data)){var i=$.remove.length+1;for(t?t.listeners+=i:t=n(e.elm,i),lt(_=e.child)&&lt(_=_._vnode)&&lt(_.data)&&v(_,t),_=0;_<$.remove.length;++_)$.remove[_](e,t);lt(_=e.data.hook)&&lt(_=_.remove)?_(e,t):t()}else r(e.elm)}function h(e,t,n,r,a){for(var o,s,c,u,l=0,d=0,v=t.length-1,h=t[0],g=t[v],y=n.length-1,_=n[0],b=n[y],$=!a;l<=v&&d<=y;)ut(h)?h=t[++l]:ut(g)?g=t[--v]:ft(h,_)?(m(h,_,r),h=t[++l],_=n[++d]):ft(g,b)?(m(g,b,r),g=t[--v],b=n[--y]):ft(h,b)?(m(h,b,r),$&&C.insertBefore(e,h.elm,C.nextSibling(g.elm)),h=t[++l],b=n[--y]):ft(g,_)?(m(g,_,r),$&&C.insertBefore(e,g.elm,h.elm),g=t[--v],_=n[++d]):(ut(o)&&(o=dt(t,l,v)),s=lt(_.key)?o[_.key]:null,ut(s)?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(c=t[s],c.tag!==_.tag?(C.insertBefore(e,i(_,r),h.elm),_=n[++d]):(m(c,_,r),t[s]=void 0,$&&C.insertBefore(e,_.elm,h.elm),_=n[++d])));l>v?(u=ut(n[y+1])?null:n[y+1].elm,f(e,u,n,d,y,r)):d>y&&p(e,t,l,v)}function m(e,t,n,r){if(e!==t){if(t.isStatic&&e.isStatic&&t.key===e.key&&t.isCloned)return void(t.elm=e.elm);var i,a=t.data,o=lt(a);o&&lt(i=a.hook)&&lt(i=i.prepatch)&&i(e,t);var c=t.elm=e.elm,u=e.children,l=t.children;if(o&&s(t)){for(i=0;i<$.update.length;++i)$.update[i](e,t);lt(i=a.hook)&&lt(i=i.update)&&i(e,t)}ut(t.text)?lt(u)&&lt(l)?u!==l&&h(c,u,l,n,r):lt(l)?(lt(e.text)&&C.setTextContent(c,""),f(c,null,l,0,l.length-1,n)):lt(u)?p(c,u,0,u.length-1):lt(e.text)&&C.setTextContent(c,""):e.text!==t.text&&C.setTextContent(c,t.text),o&&lt(i=a.hook)&&lt(i=i.postpatch)&&i(e,t)}}function g(e,t,n){if(n&&e.parent)e.parent.data.pendingInsert=t;else for(var r=0;r<t.length;++r)t[r].data.hook.insert(t[r])}function y(e,t,n){t.elm=e;var r=t.tag,i=t.data,a=t.children;if(lt(i)&&(lt(_=i.hook)&&lt(_=_.init)&&_(t,!0),lt(_=t.child)))return u(t,n),!0;if(lt(r)){if(lt(a)){var s=C.childNodes(e);if(s.length){var l=!0;if(s.length!==a.length)l=!1;else for(var f=0;f<a.length;f++)if(!y(s[f],a[f],n)){l=!1;break}if(!l)return!1}else o(t,a,n)}lt(i)&&c(t,n)}return!0}var _,b,$={},w=e.modules,C=e.nodeOps;for(_=0;_<Pi.length;++_)for($[Pi[_]]=[],b=0;b<w.length;++b)void 0!==w[b][Pi[_]]&&$[Pi[_]].push(w[b][Pi[_]]);return function(e,n,r,a){if(!n)return void(e&&d(e));var o,c,u=!1,l=[];if(e){var f=lt(e.nodeType);if(!f&&ft(e,n))m(e,n,l,a);else{if(f){if(1===e.nodeType&&e.hasAttribute("server-rendered")&&(e.removeAttribute("server-rendered"),r=!0),r&&y(e,n,l))return g(n,l,!0),e;e=t(e)}if(o=e.elm,c=C.parentNode(o),i(n,l),n.parent&&(n.parent.elm=n.elm,s(n)))for(var v=0;v<$.create.length;++v)$.create[v](Mi,n.parent);null!==c?(C.insertBefore(c,n.elm,C.nextSibling(o)),p(c,[e],0,0)):lt(e.tag)&&d(e)}}else u=!0,i(n,l);return g(n,l,u),n.elm}}function vt(e,t){if(e.data.directives||t.data.directives){var n,r,i,a=e===Mi,o=ht(e.data.directives,e.context),s=ht(t.data.directives,t.context),c=[],u=[];for(n in s)r=o[n],i=s[n],r?(i.oldValue=r.value,gt(i,"update",t,e),i.def&&i.def.componentUpdated&&u.push(i)):(gt(i,"bind",t,e),i.def&&i.def.inserted&&c.push(i));if(c.length){var l=function(){c.forEach(function(n){gt(n,"inserted",t,e)})};a?q(t.data.hook||(t.data.hook={}),"insert",l,"dir-insert"):l()}if(u.length&&q(t.data.hook||(t.data.hook={}),"postpatch",function(){u.forEach(function(n){gt(n,"componentUpdated",t,e)})},"dir-postpatch"),!a)for(n in o)s[n]||gt(o[n],"unbind",e)}}function ht(e,t){var n=Object.create(null);if(!e)return n;var r,i;for(r=0;r<e.length;r++)i=e[r],i.modifiers||(i.modifiers=Ii),n[mt(i)]=i,i.def=je(t.$options,"directives",i.name,!0);return n}function mt(e){return e.rawName||e.name+"."+Object.keys(e.modifiers||{}).join(".")}function gt(e,t,n,r){var i=e.def&&e.def[t];i&&i(n.elm,e,n,r)}function yt(e,t){if(e.data.attrs||t.data.attrs){var n,r,i,a=t.elm,o=e.data.attrs||{},s=t.data.attrs||{};s.__ob__&&(s=t.data.attrs=u({},s));for(n in s)r=s[n],i=o[n],i!==r&&_t(a,n,r);for(n in o)null==s[n]&&($i(n)?a.removeAttributeNS(bi,wi(n)):yi(n)||a.removeAttribute(n))}}function _t(e,t,n){_i(t)?Ci(n)?e.removeAttribute(t):e.setAttribute(t,t):yi(t)?e.setAttribute(t,Ci(n)||"false"===n?"false":"true"):$i(t)?Ci(n)?e.removeAttributeNS(bi,wi(t)):e.setAttributeNS(bi,t,n):Ci(n)?e.removeAttribute(t):e.setAttribute(t,n)}function bt(e,t){var n=t.elm,r=t.data,i=e.data;if(r.staticClass||r.class||i&&(i.staticClass||i.class)){var a=He(t),o=n._transitionClasses;o&&(a=Ve(a,Je(o))),a!==n._prevClass&&(n.setAttribute("class",a),n._prevClass=a)}}function $t(e,t){if(e.data.on||t.data.on){var n=t.data.on||{},r=e.data.on||{},i=t.elm._v_add||(t.elm._v_add=function(e,n,r){t.elm.addEventListener(e,n,r)}),a=t.elm._v_remove||(t.elm._v_remove=function(e,n){t.elm.removeEventListener(e,n)});K(n,r,i,a,t.context)}}function wt(e,t){if(e.data.domProps||t.data.domProps){var n,r,i=t.elm,a=e.data.domProps||{},o=t.data.domProps||{};o.__ob__&&(o=t.data.domProps=u({},o));for(n in a)null==o[n]&&(i[n]=void 0);for(n in o)if("textContent"!==n&&"innerHTML"!==n||!t.children||(t.children.length=0),r=o[n],"value"===n){i._value=r;var s=null==r?"":String(r);i.value===s||i.composing||(i.value=s)}else i[n]=r}}function Ct(e,t){if(e.data&&e.data.style||t.data.style){var n,r,i=t.elm,a=e.data.style||{},o=t.data.style||{};if("string"==typeof o)return void(i.style.cssText=o);var s=o.__ob__;Array.isArray(o)&&(o=t.data.style=d(o)),s&&(o=t.data.style=u({},o));for(r in a)null==o[r]&&(i.style[Ji(r)]="");for(r in o)n=o[r],n!==a[r]&&(i.style[Ji(r)]=null==n?"":n)}}function xt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.add(t)}):e.classList.add(t);else{var n=" "+e.getAttribute("class")+" ";n.indexOf(" "+t+" ")<0&&e.setAttribute("class",(n+t).trim())}}function kt(e,t){if(e.classList)t.indexOf(" ")>-1?t.split(/\s+/).forEach(function(t){return e.classList.remove(t)}):e.classList.remove(t);else{for(var n=" "+e.getAttribute("class")+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");e.setAttribute("class",n.trim())}}function At(e){ea(function(){ea(e)})}function Ot(e,t){(e._transitionClasses||(e._transitionClasses=[])).push(t),xt(e,t)}function Tt(e,t){e._transitionClasses&&r(e._transitionClasses,t),kt(e,t)}function St(e,t,n){var r=Et(e,t),i=r.type,a=r.timeout,o=r.propCount;if(!i)return n();var s=i===Wi?Yi:Xi,c=0,u=function(){e.removeEventListener(s,l),n()},l=function(t){t.target===e&&++c>=o&&u()};setTimeout(function(){c<o&&u()},a+1),e.addEventListener(s,l)}function Et(e,t){var n,r=window.getComputedStyle(e),i=r[Gi+"Delay"].split(", "),a=r[Gi+"Duration"].split(", "),o=jt(i,a),s=r[Qi+"Delay"].split(", "),c=r[Qi+"Duration"].split(", "),u=jt(s,c),l=0,f=0;t===Wi?o>0&&(n=Wi,l=o,f=a.length):t===Zi?u>0&&(n=Zi,l=u,f=c.length):(l=Math.max(o,u),n=l>0?o>u?Wi:Zi:null,f=n?n===Wi?a.length:c.length:0);var d=n===Wi&&ta.test(r[Gi+"Property"]);return{type:n,timeout:l,propCount:f,hasTransform:d}}function jt(e,t){return Math.max.apply(null,t.map(function(t,n){return Lt(t)+Lt(e[n])}))}function Lt(e){return 1e3*Number(e.slice(0,-1))}function Nt(e){var t=e.elm;t._leaveCb&&(t._leaveCb.cancelled=!0,t._leaveCb());var n=Mt(e.data.transition);if(n&&!t._enterCb&&1===t.nodeType){var r=n.css,i=n.type,a=n.enterClass,o=n.enterActiveClass,s=n.appearClass,c=n.appearActiveClass,u=n.beforeEnter,l=n.enter,f=n.afterEnter,d=n.enterCancelled,p=n.beforeAppear,v=n.appear,h=n.afterAppear,m=n.appearCancelled,g=ai.$vnode,y=g&&g.parent?g.parent.context:ai,_=!y._isMounted||!e.isRootInsert;if(!_||v||""===v){var b=_?s:a,$=_?c:o,w=_?p||u:u,C=_&&"function"==typeof v?v:l,x=_?h||f:f,k=_?m||d:d,A=r!==!1&&!Dr,O=C&&(C._length||C.length)>1,T=t._enterCb=Pt(function(){A&&Tt(t,$),T.cancelled?(A&&Tt(t,b),k&&k(t)):x&&x(t),t._enterCb=null});e.data.show||q(e.data.hook||(e.data.hook={}),"insert",function(){var n=t.parentNode,r=n&&n._pending&&n._pending[e.key];r&&r.tag===e.tag&&r.elm._leaveCb&&r.elm._leaveCb(),C&&C(t,T)},"transition-insert"),w&&w(t),A&&(Ot(t,b),Ot(t,$),At(function(){Tt(t,b),T.cancelled||O||St(t,i,T)})),e.data.show&&C&&C(t,T),A||O||T()}}}function Dt(e,t){function n(){m.cancelled||(e.data.show||((r.parentNode._pending||(r.parentNode._pending={}))[e.key]=e),u&&u(r),v&&(Ot(r,s),Ot(r,c),At(function(){Tt(r,s),m.cancelled||h||St(r,o,m)})),l&&l(r,m),v||h||m())}var r=e.elm;r._enterCb&&(r._enterCb.cancelled=!0,r._enterCb());var i=Mt(e.data.transition);if(!i)return t();if(!r._leaveCb&&1===r.nodeType){var a=i.css,o=i.type,s=i.leaveClass,c=i.leaveActiveClass,u=i.beforeLeave,l=i.leave,f=i.afterLeave,d=i.leaveCancelled,p=i.delayLeave,v=a!==!1&&!Dr,h=l&&(l._length||l.length)>1,m=r._leaveCb=Pt(function(){r.parentNode&&r.parentNode._pending&&(r.parentNode._pending[e.key]=null),v&&Tt(r,c),m.cancelled?(v&&Tt(r,s),d&&d(r)):(t(),f&&f(r)),r._leaveCb=null});p?p(n):n()}}function Mt(e){if(e){if("object"==typeof e){var t={};return e.css!==!1&&u(t,na(e.name||"v")),u(t,e),t}return"string"==typeof e?na(e):void 0}}function Pt(e){var t=!1;return function(){t||(t=!0,e())}}function Rt(e,t,n){var r=t.value,i=e.multiple;if(!i||Array.isArray(r)){for(var a,o,s=0,c=e.options.length;s<c;s++)if(o=e.options[s],i)a=m(r,Bt(o))>-1,o.selected!==a&&(o.selected=a);else if(h(Bt(o),r))return void(e.selectedIndex!==s&&(e.selectedIndex=s));i||(e.selectedIndex=-1)}}function It(e,t){for(var n=0,r=t.length;n<r;n++)if(h(Bt(t[n]),e))return!1;return!0}function Bt(e){return"_value"in e?e._value:e.value}function Ft(e){e.target.composing=!0}function Ht(e){e.target.composing=!1,Ut(e.target,"input")}function Ut(e,t){var n=document.createEvent("HTMLEvents");n.initEvent(t,!0,!0),e.dispatchEvent(n)}function zt(e){return!e.child||e.data&&e.data.transition?e:zt(e.child._vnode)}function Vt(e){var t=e&&e.componentOptions;return t&&t.Ctor.options.abstract?Vt(X(t.children)):e}function Jt(e){var t={},n=e.$options;for(var r in n.propsData)t[r]=e[r];var i=n._parentListeners;for(var a in i)t[$r(a)]=i[a].fn;return t}function qt(e,t){return/\d-keep-alive$/.test(t.tag)?e("keep-alive"):null}function Kt(e){for(;e=e.parent;)if(e.data.transition)return!0}function Wt(e){e.elm._moveCb&&e.elm._moveCb(),e.elm._enterCb&&e.elm._enterCb();
+}function Zt(e){e.data.newPos=e.elm.getBoundingClientRect()}function Gt(e){var t=e.data.pos,n=e.data.newPos,r=t.left-n.left,i=t.top-n.top;if(r||i){e.data.moved=!0;var a=e.elm.style;a.transform=a.WebkitTransform="translate("+r+"px,"+i+"px)",a.transitionDuration="0s"}}function Yt(e,t){var n=document.createElement("div");return n.innerHTML='<div a="'+e+'">',n.innerHTML.indexOf(t)>0}function Qt(e){return ma.innerHTML=e,ma.textContent}function Xt(e,t){return t&&(e=e.replace(Za,"\n")),e.replace(Ka,"<").replace(Wa,">").replace(Ga,"&").replace(Ya,'"')}function en(e,t){function n(t){f+=t,e=e.substring(t)}function r(){var t=e.match(Ca);if(t){var r={tagName:t[1],attrs:[],start:f};n(t[0].length);for(var i,a;!(i=e.match(xa))&&(a=e.match(ba));)n(a[0].length),r.attrs.push(a);if(i)return r.unarySlash=i[1],n(i[0].length),r.end=f,r}}function i(e){var n=e.tagName,r=e.unarySlash;u&&("p"===s&&Ti(n)&&a("",s),Oi(n)&&s===n&&a("",n));for(var i=l(n)||"html"===n&&"head"===s||!!r,o=e.attrs.length,f=new Array(o),d=0;d<o;d++){var p=e.attrs[d];Oa&&p[0].indexOf('""')===-1&&(""===p[3]&&delete p[3],""===p[4]&&delete p[4],""===p[5]&&delete p[5]);var v=p[3]||p[4]||p[5]||"";f[d]={name:p[1],value:Xt(v,t.shouldDecodeNewlines)}}i||(c.push({tag:n,attrs:f}),s=n,r=""),t.start&&t.start(n,f,i,e.start,e.end)}function a(e,n,r,i){var a;if(null==r&&(r=f),null==i&&(i=f),n){var o=n.toLowerCase();for(a=c.length-1;a>=0&&c[a].tag.toLowerCase()!==o;a--);}else a=0;if(a>=0){for(var u=c.length-1;u>=a;u--)t.end&&t.end(c[u].tag,r,i);c.length=a,s=a&&c[a-1].tag}else"br"===n.toLowerCase()?t.start&&t.start(n,[],!0,r,i):"p"===n.toLowerCase()&&(t.start&&t.start(n,[],!1,r,i),t.end&&t.end(n,r,i))}for(var o,s,c=[],u=t.expectHTML,l=t.isUnaryTag||Or,f=0;e;){if(o=e,s&&Ja(s)){var d=s.toLowerCase(),p=qa[d]||(qa[d]=new RegExp("([\\s\\S]*?)(</"+d+"[^>]*>)","i")),v=0,h=e.replace(p,function(e,n,r){return v=r.length,"script"!==d&&"style"!==d&&"noscript"!==d&&(n=n.replace(/<!--([\s\S]*?)-->/g,"$1").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g,"$1")),t.chars&&t.chars(n),""});f+=e.length-h.length,e=h,a("</"+d+">",d,f-v,f)}else{var m=e.indexOf("<");if(0===m){if(/^<!--/.test(e)){var g=e.indexOf("-->");if(g>=0){n(g+3);continue}}if(/^<!\[/.test(e)){var y=e.indexOf("]>");if(y>=0){n(y+2);continue}}var _=e.match(Aa);if(_){n(_[0].length);continue}var b=e.match(ka);if(b){var $=f;n(b[0].length),a(b[0],b[1],$,f);continue}var w=r();if(w){i(w);continue}}var C=void 0;m>=0?(C=e.substring(0,m),n(m)):(C=e,e=""),t.chars&&t.chars(C)}if(e===o)throw new Error("Error parsing template:\n\n"+e)}a()}function tn(e){function t(){(o||(o=[])).push(e.slice(d,i).trim()),d=i+1}var n,r,i,a,o,s=!1,c=!1,u=0,l=0,f=0,d=0;for(i=0;i<e.length;i++)if(r=n,n=e.charCodeAt(i),s)39===n&&92!==r&&(s=!s);else if(c)34===n&&92!==r&&(c=!c);else if(124!==n||124===e.charCodeAt(i+1)||124===e.charCodeAt(i-1)||u||l||f)switch(n){case 34:c=!0;break;case 39:s=!0;break;case 40:f++;break;case 41:f--;break;case 91:l++;break;case 93:l--;break;case 123:u++;break;case 125:u--}else void 0===a?(d=i+1,a=e.slice(0,i).trim()):t();if(void 0===a?a=e.slice(0,i).trim():0!==d&&t(),o)for(i=0;i<o.length;i++)a=nn(a,o[i]);return a}function nn(e,t){var n=t.indexOf("(");if(n<0)return'_f("'+t+'")('+e+")";var r=t.slice(0,n),i=t.slice(n+1);return'_f("'+r+'")('+e+","+i}function rn(e,t){var n=t?eo(t):Qa;if(n.test(e)){for(var r,i,a=[],o=n.lastIndex=0;r=n.exec(e);){i=r.index,i>o&&a.push(JSON.stringify(e.slice(o,i)));var s=tn(r[1].trim());a.push("_s("+s+")"),o=i+r[0].length}return o<e.length&&a.push(JSON.stringify(e.slice(o))),a.join("+")}}function an(e){console.error("[Vue parser]: "+e)}function on(e,t){return e?e.map(function(e){return e[t]}).filter(function(e){return e}):[]}function sn(e,t,n){(e.props||(e.props=[])).push({name:t,value:n})}function cn(e,t,n){(e.attrs||(e.attrs=[])).push({name:t,value:n})}function un(e,t,n,r,i,a){(e.directives||(e.directives=[])).push({name:t,rawName:n,value:r,arg:i,modifiers:a})}function ln(e,t,n,r,i){r&&r.capture&&(delete r.capture,t="!"+t);var a;r&&r.native?(delete r.native,a=e.nativeEvents||(e.nativeEvents={})):a=e.events||(e.events={});var o={value:n,modifiers:r},s=a[t];Array.isArray(s)?i?s.unshift(o):s.push(o):s?a[t]=i?[o,s]:[s,o]:a[t]=o}function fn(e,t,n){var r=dn(e,":"+t)||dn(e,"v-bind:"+t);if(null!=r)return r;if(n!==!1){var i=dn(e,t);if(null!=i)return JSON.stringify(i)}}function dn(e,t){var n;if(null!=(n=e.attrsMap[t]))for(var r=e.attrsList,i=0,a=r.length;i<a;i++)if(r[i].name===t){r.splice(i,1);break}return n}function pn(e,t){Ta=t.warn||an,Sa=t.getTagNamespace||Or,Ea=t.mustUseProp||Or,ja=t.isPreTag||Or,La=on(t.modules,"preTransformNode"),Na=on(t.modules,"transformNode"),Da=on(t.modules,"postTransformNode"),Ma=t.delimiters;var n,r,i=[],a=t.preserveWhitespace!==!1,o=!1,s=!1;return en(e,{expectHTML:t.expectHTML,isUnaryTag:t.isUnaryTag,shouldDecodeNewlines:t.shouldDecodeNewlines,start:function(e,a,c){function u(e){}var l=r&&r.ns||Sa(e);t.isIE&&"svg"===l&&(a=En(a));var f={type:1,tag:e,attrsList:a,attrsMap:On(a,t.isIE),parent:r,children:[]};l&&(f.ns=l),Sn(f)&&(f.forbidden=!0);for(var d=0;d<La.length;d++)La[d](f,t);if(o||(vn(f),f.pre&&(o=!0)),ja(f.tag)&&(s=!0),o)hn(f);else{yn(f),_n(f),$n(f),mn(f),f.plain=!f.key&&!a.length,gn(f),wn(f),Cn(f);for(var p=0;p<Na.length;p++)Na[p](f,t);xn(f)}n||(n=f,u(n)),r&&!f.forbidden&&(f.else?bn(f,r):(r.children.push(f),f.parent=r)),c||(r=f,i.push(f));for(var v=0;v<Da.length;v++)Da[v](f,t)},end:function(){var e=i[i.length-1],t=e.children[e.children.length-1];t&&3===t.type&&" "===t.text&&e.children.pop(),i.length-=1,r=i[i.length-1],e.pre&&(o=!1),ja(e.tag)&&(s=!1)},chars:function(e){if(r&&(e=s||e.trim()?uo(e):a&&r.children.length?" ":"")){var t;!o&&" "!==e&&(t=rn(e,Ma))?r.children.push({type:2,expression:t,text:e}):(e=e.replace(co,""),r.children.push({type:3,text:e}))}}}),n}function vn(e){null!=dn(e,"v-pre")&&(e.pre=!0)}function hn(e){var t=e.attrsList.length;if(t)for(var n=e.attrs=new Array(t),r=0;r<t;r++)n[r]={name:e.attrsList[r].name,value:JSON.stringify(e.attrsList[r].value)};else e.pre||(e.plain=!0)}function mn(e){var t=fn(e,"key");t&&(e.key=t)}function gn(e){var t=fn(e,"ref");t&&(e.ref=t,e.refInFor=kn(e))}function yn(e){var t;if(t=dn(e,"v-for")){var n=t.match(no);if(!n)return;e.for=n[2].trim();var r=n[1].trim(),i=r.match(ro);i?(e.alias=i[1].trim(),e.iterator1=i[2].trim(),i[3]&&(e.iterator2=i[3].trim())):e.alias=r}}function _n(e){var t=dn(e,"v-if");t&&(e.if=t),null!=dn(e,"v-else")&&(e.else=!0)}function bn(e,t){var n=Tn(t.children);n&&n.if&&(n.elseBlock=e)}function $n(e){var t=dn(e,"v-once");null!=t&&(e.once=!0)}function wn(e){if("slot"===e.tag)e.slotName=fn(e,"name");else{var t=fn(e,"slot");t&&(e.slotTarget=t)}}function Cn(e){var t;(t=fn(e,"is"))&&(e.component=t),null!=dn(e,"inline-template")&&(e.inlineTemplate=!0)}function xn(e){var t,n,r,i,a,o,s,c,u=e.attrsList;for(t=0,n=u.length;t<n;t++)if(r=i=u[t].name,a=u[t].value,to.test(r))if(e.hasBindings=!0,s=An(r),s&&(r=r.replace(so,"")),io.test(r))r=r.replace(io,""),s&&s.prop&&(c=!0,r=$r(r),"innerHtml"===r&&(r="innerHTML")),c||Ea(r)?sn(e,r,a):cn(e,r,a);else if(ao.test(r))r=r.replace(ao,""),ln(e,r,a,s);else{r=r.replace(to,"");var l=r.match(oo);l&&(o=l[1])&&(r=r.slice(0,-(o.length+1))),un(e,r,i,a,o,s)}else cn(e,r,JSON.stringify(a))}function kn(e){for(var t=e;t;){if(void 0!==t.for)return!0;t=t.parent}return!1}function An(e){var t=e.match(so);if(t){var n={};return t.forEach(function(e){n[e.slice(1)]=!0}),n}}function On(e,t){for(var n={},r=0,i=e.length;r<i;r++)n[e[r].name]=e[r].value;return n}function Tn(e){for(var t=e.length;t--;)if(e[t].tag)return e[t]}function Sn(e){return"style"===e.tag||"script"===e.tag&&(!e.attrsMap.type||"text/javascript"===e.attrsMap.type)}function En(e){for(var t=[],n=0;n<e.length;n++){var r=e[n];lo.test(r.name)||(r.name=r.name.replace(fo,""),t.push(r))}return t}function jn(e,t){e&&(Pa=po(t.staticKeys||""),Ra=t.isReservedTag||function(){return!1},Nn(e),Dn(e,!1))}function Ln(e){return n("type,tag,attrsList,attrsMap,plain,parent,children,attrs"+(e?","+e:""))}function Nn(e){if(e.static=Mn(e),1===e.type)for(var t=0,n=e.children.length;t<n;t++){var r=e.children[t];Nn(r),r.static||(e.static=!1)}}function Dn(e,t){if(1===e.type){if(e.once||e.static)return e.staticRoot=!0,void(e.staticInFor=t);if(e.children)for(var n=0,r=e.children.length;n<r;n++)Dn(e.children[n],t||!!e.for)}}function Mn(e){return 2!==e.type&&(3===e.type||!(!e.pre&&(e.hasBindings||e.if||e.for||yr(e.tag)||!Ra(e.tag)||Pn(e)||!Object.keys(e).every(Pa))))}function Pn(e){for(;e.parent;){if(e=e.parent,"template"!==e.tag)return!1;if(e.for)return!0}return!1}function Rn(e,t){var n=t?"nativeOn:{":"on:{";for(var r in e)n+='"'+r+'":'+In(e[r])+",";return n.slice(0,-1)+"}"}function In(e){if(e){if(Array.isArray(e))return"["+e.map(In).join(",")+"]";if(e.modifiers){var t="",n=[];for(var r in e.modifiers)mo[r]?t+=mo[r]:n.push(r);n.length&&(t=Bn(n)+t);var i=vo.test(e.value)?e.value+"($event)":e.value;return"function($event){"+t+i+"}"}return vo.test(e.value)?e.value:"function($event){"+e.value+"}"}return"function(){}"}function Bn(e){var t=1===e.length?Fn(e[0]):Array.prototype.concat.apply([],e.map(Fn));return Array.isArray(t)?"if("+t.map(function(e){return"$event.keyCode!=="+e}).join("&&")+")return;":"if($event.keyCode!=="+t+")return;"}function Fn(e){return parseInt(e,10)||ho[e]||"_k("+JSON.stringify(e)+")"}function Hn(e,t){e.wrapData=function(e){return"_b("+e+","+t.value+(t.modifiers&&t.modifiers.prop?",true":"")+")"}}function Un(e,t){var n=Ua,r=Ua=[];za=t,Ia=t.warn||an,Ba=on(t.modules,"transformCode"),Fa=on(t.modules,"genData"),Ha=t.directives||{};var i=e?zn(e):'_h("div")';return Ua=n,{render:"with(this){return "+i+"}",staticRenderFns:r}}function zn(e){if(e.staticRoot&&!e.staticProcessed)return e.staticProcessed=!0,Ua.push("with(this){return "+zn(e)+"}"),"_m("+(Ua.length-1)+(e.staticInFor?",true":"")+")";if(e.for&&!e.forProcessed)return qn(e);if(e.if&&!e.ifProcessed)return Vn(e);if("template"!==e.tag||e.slotTarget){if("slot"===e.tag)return Qn(e);var t;if(e.component)t=Xn(e);else{var n=Kn(e),r=e.inlineTemplate?null:Zn(e);t="_h('"+e.tag+"'"+(n?","+n:"")+(r?","+r:"")+")"}for(var i=0;i<Ba.length;i++)t=Ba[i](e,t);return t}return Zn(e)||"void 0"}function Vn(e){var t=e.if;return e.ifProcessed=!0,"("+t+")?"+zn(e)+":"+Jn(e)}function Jn(e){return e.elseBlock?zn(e.elseBlock):"_e()"}function qn(e){var t=e.for,n=e.alias,r=e.iterator1?","+e.iterator1:"",i=e.iterator2?","+e.iterator2:"";return e.forProcessed=!0,"_l(("+t+"),function("+n+r+i+"){return "+zn(e)+"})"}function Kn(e){if(!e.plain){var t="{",n=Wn(e);n&&(t+=n+","),e.key&&(t+="key:"+e.key+","),e.ref&&(t+="ref:"+e.ref+","),e.refInFor&&(t+="refInFor:true,"),e.component&&(t+='tag:"'+e.tag+'",'),e.slotTarget&&(t+="slot:"+e.slotTarget+",");for(var r=0;r<Fa.length;r++)t+=Fa[r](e);if(e.attrs&&(t+="attrs:{"+er(e.attrs)+"},"),e.props&&(t+="domProps:{"+er(e.props)+"},"),e.events&&(t+=Rn(e.events)+","),e.nativeEvents&&(t+=Rn(e.nativeEvents,!0)+","),e.inlineTemplate){var i=e.children[0];if(1===i.type){var a=Un(i,za);t+="inlineTemplate:{render:function(){"+a.render+"},staticRenderFns:["+a.staticRenderFns.map(function(e){return"function(){"+e+"}"}).join(",")+"]}"}}return t=t.replace(/,$/,"")+"}",e.wrapData&&(t=e.wrapData(t)),t}}function Wn(e){var t=e.directives;if(t){var n,r,i,a,o="directives:[",s=!1;for(n=0,r=t.length;n<r;n++){i=t[n],a=!0;var c=Ha[i.name]||go[i.name];c&&(a=!!c(e,i,Ia)),a&&(s=!0,o+='{name:"'+i.name+'",rawName:"'+i.rawName+'"'+(i.value?",value:("+i.value+"),expression:"+JSON.stringify(i.value):"")+(i.arg?',arg:"'+i.arg+'"':"")+(i.modifiers?",modifiers:"+JSON.stringify(i.modifiers):"")+"},")}return s?o.slice(0,-1)+"]":void 0}}function Zn(e){if(e.children.length)return"["+e.children.map(Gn).join(",")+"]"}function Gn(e){return 1===e.type?zn(e):Yn(e)}function Yn(e){return 2===e.type?e.expression:JSON.stringify(e.text)}function Qn(e){var t=e.slotName||'"default"',n=Zn(e);return n?"_t("+t+","+n+")":"_t("+t+")"}function Xn(e){var t=e.inlineTemplate?null:Zn(e);return"_h("+e.component+","+Kn(e)+(t?","+t:"")+")"}function er(e){for(var t="",n=0;n<e.length;n++){var r=e[n];t+='"'+r.name+'":'+r.value+","}return t.slice(0,-1)}function tr(e,t){var n=pn(e.trim(),t);jn(n,t);var r=Un(n,t);return{ast:n,render:r.render,staticRenderFns:r.staticRenderFns}}function nr(e,t){var n=(t.warn||an,dn(e,"class"));n&&(e.staticClass=JSON.stringify(n));var r=fn(e,"class",!1);r&&(e.classBinding=r)}function rr(e){var t="";return e.staticClass&&(t+="staticClass:"+e.staticClass+","),e.classBinding&&(t+="class:"+e.classBinding+","),t}function ir(e){var t=fn(e,"style",!1);t&&(e.styleBinding=t)}function ar(e){return e.styleBinding?"style:("+e.styleBinding+"),":""}function or(e,t,n){Va=n;var r=t.value,i=t.modifiers,a=e.tag,o=e.attrsMap.type;return"select"===a?lr(e,r):"input"===a&&"checkbox"===o?sr(e,r):"input"===a&&"radio"===o?cr(e,r):ur(e,r,i),!0}function sr(e,t){var n=fn(e,"value")||"null",r=fn(e,"true-value")||"true",i=fn(e,"false-value")||"false";sn(e,"checked","Array.isArray("+t+")?_i("+t+","+n+")>-1:_q("+t+","+r+")"),ln(e,"change","var $$a="+t+",$$el=$event.target,$$c=$$el.checked?("+r+"):("+i+");if(Array.isArray($$a)){var $$v="+n+",$$i=_i($$a,$$v);if($$c){$$i<0&&("+t+"=$$a.concat($$v))}else{$$i>-1&&("+t+"=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{"+t+"=$$c}",null,!0)}function cr(e,t){var n=fn(e,"value")||"null";sn(e,"checked","_q("+t+","+n+")"),ln(e,"change",t+"="+n,null,!0)}function ur(e,t,n){var r=e.attrsMap.type,i=n||{},a=i.lazy,o=i.number,s=i.trim,c=a||Nr&&"range"===r?"change":"input",u=!a&&"range"!==r,l="input"===e.tag||"textarea"===e.tag,f=l?"$event.target.value"+(s?".trim()":""):"$event",d=o||"number"===r?t+"=_n("+f+")":t+"="+f;l&&u&&(d="if($event.target.composing)return;"+d),sn(e,"value",l?"_s("+t+")":"("+t+")"),ln(e,c,d,null,!0)}function lr(e,t){var n=t+'=Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){return "_value" in o ? o._value : o.value})'+(null==e.attrsMap.multiple?"[0]":"");ln(e,"change",n,null,!0)}function fr(e,t){t.value&&sn(e,"textContent","_s("+t.value+")")}function dr(e,t){t.value&&sn(e,"innerHTML","_s("+t.value+")")}function pr(e,t){return t=t?u(u({},Co),t):Co,tr(e,t)}function vr(e,t,n){var r=(t&&t.warn||li,t&&t.delimiters?String(t.delimiters)+e:e);if(wo[r])return wo[r];var i={},a=pr(e,t);i.render=hr(a.render);var o=a.staticRenderFns.length;i.staticRenderFns=new Array(o);for(var s=0;s<o;s++)i.staticRenderFns[s]=hr(a.staticRenderFns[s]);return wo[r]=i}function hr(e){try{return new Function(e)}catch(e){return p}}function mr(e){if(e.outerHTML)return e.outerHTML;var t=document.createElement("div");return t.appendChild(e.cloneNode(!0)),t.innerHTML}var gr,yr=n("slot,component",!0),_r=Object.prototype.hasOwnProperty,br=/-(\w)/g,$r=o(function(e){return e.replace(br,function(e,t){return t?t.toUpperCase():""})}),wr=o(function(e){return e.charAt(0).toUpperCase()+e.slice(1)}),Cr=/([^-])([A-Z])/g,xr=o(function(e){return e.replace(Cr,"$1-$2").replace(Cr,"$1-$2").toLowerCase()}),kr=Object.prototype.toString,Ar="[object Object]",Or=function(){return!1},Tr={optionMergeStrategies:Object.create(null),silent:!1,devtools:!1,errorHandler:null,ignoredElements:null,keyCodes:Object.create(null),isReservedTag:Or,isUnknownElement:Or,getTagNamespace:p,mustUseProp:Or,_assetTypes:["component","directive","filter"],_lifecycleHooks:["beforeCreate","created","beforeMount","mounted","beforeUpdate","updated","beforeDestroy","destroyed","activated","deactivated"],_maxUpdateCount:100,_isServer:!1},Sr=/[^\w\.\$]/,Er="__proto__"in{},jr="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Lr=jr&&window.navigator.userAgent.toLowerCase(),Nr=Lr&&/msie|trident/.test(Lr),Dr=Lr&&Lr.indexOf("msie 9.0")>0,Mr=Lr&&Lr.indexOf("edge/")>0,Pr=Lr&&Lr.indexOf("android")>0,Rr=Lr&&/iphone|ipad|ipod|ios/.test(Lr),Ir=jr&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Br=function(){function e(){r=!1;var e=n.slice(0);n.length=0;for(var t=0;t<e.length;t++)e[t]()}var t,n=[],r=!1;if("undefined"!=typeof Promise&&b(Promise)){var i=Promise.resolve();t=function(){i.then(e),Rr&&setTimeout(p)}}else if("undefined"==typeof MutationObserver||!b(MutationObserver)&&"[object MutationObserverConstructor]"!==MutationObserver.toString())t=function(){setTimeout(e,0)};else{var a=1,o=new MutationObserver(e),s=document.createTextNode(String(a));o.observe(s,{characterData:!0}),t=function(){a=(a+1)%2,s.data=String(a)}}return function(e,i){var a=i?function(){e.call(i)}:e;n.push(a),r||(r=!0,t())}}();gr="undefined"!=typeof Set&&b(Set)?Set:function(){function e(){this.set=Object.create(null)}return e.prototype.has=function(e){return void 0!==this.set[e]},e.prototype.add=function(e){this.set[e]=1},e.prototype.clear=function(){this.set=Object.create(null)},e}();var Fr=0,Hr=function(){this.id=Fr++,this.subs=[]};Hr.prototype.addSub=function(e){this.subs.push(e)},Hr.prototype.removeSub=function(e){r(this.subs,e)},Hr.prototype.depend=function(){Hr.target&&Hr.target.addDep(this)},Hr.prototype.notify=function(){for(var e=this.subs.slice(),t=0,n=e.length;t<n;t++)e[t].update()},Hr.target=null;var Ur=[],zr=[],Vr={},Jr=!1,qr=!1,Kr=0,Wr=0,Zr=function(e,t,n,r){void 0===r&&(r={}),this.vm=e,e._watchers.push(this),this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.expression=t.toString(),this.cb=n,this.id=++Wr,this.active=!0,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new gr,this.newDepIds=new gr,"function"==typeof t?this.getter=t:(this.getter=_(t),this.getter||(this.getter=function(){})),this.value=this.lazy?void 0:this.get()};Zr.prototype.get=function(){$(this);var e=this.getter.call(this.vm,this.vm);return this.deep&&A(e),w(),this.cleanupDeps(),e},Zr.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},Zr.prototype.cleanupDeps=function(){for(var e=this,t=this.deps.length;t--;){var n=e.deps[t];e.newDepIds.has(n.id)||n.removeSub(e)}var r=this.depIds;this.depIds=this.newDepIds,this.newDepIds=r,this.newDepIds.clear(),r=this.deps,this.deps=this.newDeps,this.newDeps=r,this.newDeps.length=0},Zr.prototype.update=function(){this.lazy?this.dirty=!0:this.sync?this.run():k(this)},Zr.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||l(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){if(!Tr.errorHandler)throw e;Tr.errorHandler.call(null,e,this.vm)}else this.cb.call(this.vm,e,t)}}},Zr.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},Zr.prototype.depend=function(){for(var e=this,t=this.deps.length;t--;)e.deps[t].depend()},Zr.prototype.teardown=function(){var e=this;if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||r(this.vm._watchers,this);for(var t=this.deps.length;t--;)e.deps[t].removeSub(e);this.active=!1}};var Gr=new gr,Yr=Array.prototype,Qr=Object.create(Yr);["push","pop","shift","unshift","splice","sort","reverse"].forEach(function(e){var t=Yr[e];y(Qr,e,function(){for(var n=arguments,r=arguments.length,i=new Array(r);r--;)i[r]=n[r];var a,o=t.apply(this,i),s=this.__ob__;switch(e){case"push":a=i;break;case"unshift":a=i;break;case"splice":a=i.slice(2)}return a&&s.observeArray(a),s.dep.notify(),o})});var Xr=Object.getOwnPropertyNames(Qr),ei={shouldConvert:!0,isSettingProps:!1},ti=function(e){if(this.value=e,this.dep=new Hr,this.vmCount=0,y(e,"__ob__",this),Array.isArray(e)){var t=Er?O:T;t(e,Qr,Xr),this.observeArray(e)}else this.walk(e)};ti.prototype.walk=function(e){for(var t=Object.keys(e),n=0;n<t.length;n++)E(e,t[n],e[t[n]])},ti.prototype.observeArray=function(e){for(var t=0,n=e.length;t<n;t++)S(e[t])};var ni={enumerable:!0,configurable:!0,get:p,set:p},ri=function(e,t,n,r,i,a,o,s){this.tag=e,this.data=t,this.children=n,this.text=r,this.elm=i,this.ns=a,this.context=o,this.functionalContext=void 0,this.key=t&&t.key,this.componentOptions=s,this.child=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1},ii=function(){var e=new ri;return e.text="",e.isComment=!0,e},ai=null,oi={init:oe,prepatch:se,insert:ce,destroy:ue},si=Object.keys(oi),ci=0;we(Ce),U(Ce),$e(Ce),te(Ce),ye(Ce);var ui,li=p,fi=Tr.optionMergeStrategies;fi.data=function(e,t,n){return n?e||t?function(){var r="function"==typeof t?t.call(n):t,i="function"==typeof e?e.call(n):void 0;return r?xe(r,i):i}:void 0:t?"function"!=typeof t?e:e?function(){return xe(t.call(this),e.call(this))}:t:e},Tr._lifecycleHooks.forEach(function(e){fi[e]=ke}),Tr._assetTypes.forEach(function(e){fi[e+"s"]=Ae}),fi.watch=function(e,t){if(!t)return e;if(!e)return t;var n={};u(n,e);for(var r in t){var i=n[r],a=t[r];i&&!Array.isArray(i)&&(i=[i]),n[r]=i?i.concat(a):[a]}return n},fi.props=fi.methods=fi.computed=function(e,t){if(!t)return e;if(!e)return t;var n=Object.create(null);return u(n,e),u(n,t),n};var di=function(e,t){return void 0===t?e:t},pi=Object.freeze({defineReactive:E,_toString:e,toNumber:t,makeMap:n,isBuiltInTag:yr,remove:r,hasOwn:i,isPrimitive:a,cached:o,camelize:$r,capitalize:wr,hyphenate:xr,bind:s,toArray:c,extend:u,isObject:l,isPlainObject:f,toObject:d,noop:p,no:Or,genStaticKeys:v,looseEqual:h,looseIndexOf:m,isReserved:g,def:y,parsePath:_,hasProto:Er,inBrowser:jr,UA:Lr,isIE:Nr,isIE9:Dr,isEdge:Mr,isAndroid:Pr,isIOS:Rr,devtools:Ir,nextTick:Br,get _Set(){return gr},mergeOptions:Ee,resolveAsset:je,warn:li,formatComponentName:ui,validateProp:Le}),vi={name:"keep-alive",abstract:!0,created:function(){this.cache=Object.create(null)},render:function(){var e=X(this.$slots.default);if(e&&e.componentOptions){var t=e.componentOptions,n=null==e.key?t.Ctor.cid+"::"+t.tag:e.key;this.cache[n]?e.child=this.cache[n].child:this.cache[n]=e,e.data.keepAlive=!0}return e},destroyed:function(){var e=this;for(var t in this.cache){var n=e.cache[t];ne(n.child,"deactivated"),n.child.$destroy()}}},hi={KeepAlive:vi};Fe(Ce),Object.defineProperty(Ce.prototype,"$isServer",{get:function(){return Tr._isServer}}),Ce.version="2.0.3";var mi,gi=n("value,selected,checked,muted"),yi=n("contenteditable,draggable,spellcheck"),_i=n("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),bi="http://www.w3.org/1999/xlink",$i=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},wi=function(e){return $i(e)?e.slice(6,e.length):""},Ci=function(e){return null==e||e===!1},xi={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"},ki=n("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template"),Ai=n("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr",!0),Oi=n("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source",!0),Ti=n("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track",!0),Si=n("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font,font-face,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view",!0),Ei=function(e){return"pre"===e},ji=function(e){return ki(e)||Si(e)},Li=Object.create(null),Ni=Object.freeze({createElement:Ze,createElementNS:Ge,createTextNode:Ye,createComment:Qe,insertBefore:Xe,removeChild:et,appendChild:tt,parentNode:nt,nextSibling:rt,tagName:it,setTextContent:at,childNodes:ot,setAttribute:st}),Di={create:function(e,t){ct(t)},update:function(e,t){e.data.ref!==t.data.ref&&(ct(e,!0),ct(t))},destroy:function(e){ct(e,!0)}},Mi=new ri("",{},[]),Pi=["create","update","remove","destroy"],Ri={create:vt,update:vt,destroy:function(e){vt(e,Mi)}},Ii=Object.create(null),Bi=[Di,Ri],Fi={create:yt,update:yt},Hi={create:bt,update:bt},Ui={create:$t,update:$t},zi={create:wt,update:wt},Vi=["Webkit","Moz","ms"],Ji=o(function(e){if(mi=mi||document.createElement("div"),e=$r(e),"filter"!==e&&e in mi.style)return e;for(var t=e.charAt(0).toUpperCase()+e.slice(1),n=0;n<Vi.length;n++){var r=Vi[n]+t;if(r in mi.style)return r}}),qi={create:Ct,update:Ct},Ki=jr&&!Dr,Wi="transition",Zi="animation",Gi="transition",Yi="transitionend",Qi="animation",Xi="animationend";Ki&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Gi="WebkitTransition",Yi="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Qi="WebkitAnimation",Xi="webkitAnimationEnd"));var ea=jr&&window.requestAnimationFrame||setTimeout,ta=/\b(transform|all)(,|$)/,na=o(function(e){return{enterClass:e+"-enter",leaveClass:e+"-leave",appearClass:e+"-enter",enterActiveClass:e+"-enter-active",leaveActiveClass:e+"-leave-active",appearActiveClass:e+"-enter-active"}}),ra=jr?{create:function(e,t){t.data.show||Nt(t)},remove:function(e,t){e.data.show?t():Dt(e,t)}}:{},ia=[Fi,Hi,Ui,zi,qi,ra],aa=ia.concat(Bi),oa=pt({nodeOps:Ni,modules:aa});Dr&&document.addEventListener("selectionchange",function(){var e=document.activeElement;e&&e.vmodel&&Ut(e,"input")});var sa={inserted:function(e,t,n){if("select"===n.tag){var r=function(){Rt(e,t,n.context)};r(),(Nr||Mr)&&setTimeout(r,0)}else"textarea"!==n.tag&&"text"!==e.type||t.modifiers.lazy||(Pr||(e.addEventListener("compositionstart",Ft),e.addEventListener("compositionend",Ht)),Dr&&(e.vmodel=!0))},componentUpdated:function(e,t,n){if("select"===n.tag){Rt(e,t,n.context);var r=e.multiple?t.value.some(function(t){return It(t,e.options)}):t.value!==t.oldValue&&It(t.value,e.options);r&&Ut(e,"change")}}},ca={bind:function(e,t,n){var r=t.value;n=zt(n);var i=n.data&&n.data.transition;r&&i&&!Dr&&Nt(n);var a="none"===e.style.display?"":e.style.display;e.style.display=r?a:"none",e.__vOriginalDisplay=a},update:function(e,t,n){var r=t.value,i=t.oldValue;if(r!==i){n=zt(n);var a=n.data&&n.data.transition;a&&!Dr?r?(Nt(n),e.style.display=e.__vOriginalDisplay):Dt(n,function(){e.style.display="none"}):e.style.display=r?e.__vOriginalDisplay:"none"}}},ua={model:sa,show:ca},la={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String},fa={name:"transition",props:la,abstract:!0,render:function(e){var t=this,n=this.$slots.default;if(n&&(n=n.filter(function(e){return e.tag}),n.length)){var r=this.mode,i=n[0];if(Kt(this.$vnode))return i;var a=Vt(i);if(!a)return i;if(this._leaving)return qt(e,i);var o=a.key=null==a.key||a.isStatic?"__v"+(a.tag+this._uid)+"__":a.key,s=(a.data||(a.data={})).transition=Jt(this),c=this._vnode,l=Vt(c);if(a.data.directives&&a.data.directives.some(function(e){return"show"===e.name})&&(a.data.show=!0),l&&l.data&&l.key!==o){var f=l.data.transition=u({},s);if("out-in"===r)return this._leaving=!0,q(f,"afterLeave",function(){t._leaving=!1,t.$forceUpdate()},o),qt(e,i);if("in-out"===r){var d,p=function(){d()};q(s,"afterEnter",p,o),q(s,"enterCancelled",p,o),q(f,"delayLeave",function(e){d=e},o)}}return i}}},da=u({tag:String,moveClass:String},la);delete da.mode;var pa={props:da,render:function(e){for(var t=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],a=this.children=[],o=Jt(this),s=0;s<i.length;s++){var c=i[s];c.tag&&null!=c.key&&0!==String(c.key).indexOf("__vlist")&&(a.push(c),n[c.key]=c,(c.data||(c.data={})).transition=o)}if(r){for(var u=[],l=[],f=0;f<r.length;f++){var d=r[f];d.data.transition=o,d.data.pos=d.elm.getBoundingClientRect(),n[d.key]?u.push(d):l.push(d)}this.kept=e(t,null,u),this.removed=l}return e(t,null,a)},beforeUpdate:function(){this.__patch__(this._vnode,this.kept,!1,!0),this._vnode=this.kept},updated:function(){var e=this.prevChildren,t=this.moveClass||this.name+"-move";if(e.length&&this.hasMove(e[0].elm,t)){e.forEach(Wt),e.forEach(Zt),e.forEach(Gt);document.body.offsetHeight;e.forEach(function(e){if(e.data.moved){var n=e.elm,r=n.style;Ot(n,t),r.transform=r.WebkitTransform=r.transitionDuration="",n.addEventListener(Yi,n._moveCb=function e(r){r&&!/transform$/.test(r.propertyName)||(n.removeEventListener(Yi,e),n._moveCb=null,Tt(n,t))})}})}},methods:{hasMove:function(e,t){if(!Ki)return!1;if(null!=this._hasMove)return this._hasMove;Ot(e,t);var n=Et(e);return Tt(e,t),this._hasMove=n.hasTransform}}},va={Transition:fa,TransitionGroup:pa};Ce.config.isUnknownElement=Ke,Ce.config.isReservedTag=ji,Ce.config.getTagNamespace=qe,Ce.config.mustUseProp=gi,u(Ce.options.directives,ua),u(Ce.options.components,va),Ce.prototype.__patch__=Tr._isServer?p:oa,Ce.prototype.$mount=function(e,t){return e=e&&!Tr._isServer?We(e):void 0,this._mount(e,t)},setTimeout(function(){Tr.devtools&&Ir&&Ir.emit("init",Ce)},0);var ha=!!jr&&Yt("\n","&#10;"),ma=document.createElement("div"),ga=/([^\s"'<>\/=]+)/,ya=/(?:=)/,_a=[/"([^"]*)"+/.source,/'([^']*)'+/.source,/([^\s"'=<>`]+)/.source],ba=new RegExp("^\\s*"+ga.source+"(?:\\s*("+ya.source+")\\s*(?:"+_a.join("|")+"))?"),$a="[a-zA-Z_][\\w\\-\\.]*",wa="((?:"+$a+"\\:)?"+$a+")",Ca=new RegExp("^<"+wa),xa=/^\s*(\/?)>/,ka=new RegExp("^<\\/"+wa+"[^>]*>"),Aa=/^<!DOCTYPE [^>]+>/i,Oa=!1;"x".replace(/x(.)?/g,function(e,t){Oa=""===t});var Ta,Sa,Ea,ja,La,Na,Da,Ma,Pa,Ra,Ia,Ba,Fa,Ha,Ua,za,Va,Ja=n("script,style",!0),qa={},Ka=/&lt;/g,Wa=/&gt;/g,Za=/&#10;/g,Ga=/&amp;/g,Ya=/&quot;/g,Qa=/\{\{((?:.|\n)+?)\}\}/g,Xa=/[-.*+?^${}()|[\]\/\\]/g,eo=o(function(e){var t=e[0].replace(Xa,"\\$&"),n=e[1].replace(Xa,"\\$&");return new RegExp(t+"((?:.|\\n)+?)"+n,"g")}),to=/^v-|^@|^:/,no=/(.*?)\s+(?:in|of)\s+(.*)/,ro=/\(([^,]*),([^,]*)(?:,([^,]*))?\)/,io=/^:|^v-bind:/,ao=/^@|^v-on:/,oo=/:(.*)$/,so=/\.[^\.]+/g,co=/\u2028|\u2029/g,uo=o(Qt),lo=/^xmlns:NS\d+/,fo=/^NS\d+:/,po=o(Ln),vo=/^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*\s*$/,ho={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},mo={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:"if($event.target !== $event.currentTarget)return;"},go={bind:Hn,cloak:p},yo=(new RegExp("\\b"+"do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,super,throw,while,yield,delete,export,import,return,switch,default,extends,finally,continue,debugger,function,arguments".split(",").join("\\b|\\b")+"\\b"),{staticKeys:["staticClass"],transformNode:nr,genData:rr}),_o={transformNode:ir,genData:ar},bo=[yo,_o],$o={model:or,text:fr,html:dr},wo=Object.create(null),Co={isIE:Nr,expectHTML:!0,modules:bo,staticKeys:v(bo),directives:$o,isReservedTag:ji,isUnaryTag:Ai,mustUseProp:gi,getTagNamespace:qe,isPreTag:Ei},xo=o(function(e){var t=We(e);return t&&t.innerHTML}),ko=Ce.prototype.$mount;return Ce.prototype.$mount=function(e,t){if(e=e&&We(e),e===document.body||e===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=xo(r));else{if(!r.nodeType)return this;r=r.innerHTML}else e&&(r=mr(e));if(r){var i=vr(r,{warn:li,shouldDecodeNewlines:ha,delimiters:n.delimiters},this),a=i.render,o=i.staticRenderFns;n.render=a,n.staticRenderFns=o}}return ko.call(this,e,t)},Ce.compile=vr,Ce}); \ No newline at end of file