summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintignore4
-rw-r--r--.eslintrc4
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml85
-rw-r--r--.gitlab/issue_templates/Bug.md24
-rw-r--r--.rubocop.yml25
-rw-r--r--CHANGELOG.md28
-rw-r--r--CONTRIBUTING.md39
-rw-r--r--GITLAB_PAGES_VERSION1
-rw-r--r--Gemfile18
-rw-r--r--Gemfile.lock47
-rw-r--r--LICENSE2
-rw-r--r--PROCESS.md158
-rw-r--r--app/assets/javascripts/admin.js5
-rw-r--r--app/assets/javascripts/application.js114
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/behaviors/autosize.js3
-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.js26
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es63
-rw-r--r--app/assets/javascripts/blob/blob_dockerfile_selector.js.es63
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.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/boards/boards_bundle.js.es663
-rw-r--r--app/assets/javascripts/boards/components/board.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es634
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es65
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es617
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js.es6111
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js.es670
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js.es649
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/label.js.es654
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/milestone.js.es655
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/user.js.es696
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js.es683
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js.es690
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js.es6163
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js.es6159
-rw-r--r--app/assets/javascripts/boards/components/modal/lists_dropdown.js.es656
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js.es647
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js.es659
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/modal_mixins.js.es614
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es63
-rw-r--r--app/assets/javascripts/boards/models/list.js.es62
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es629
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es69
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js.es6107
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/breakpoints.js1
-rw-r--r--app/assets/javascripts/build.js21
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es626
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js.es629
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_store.js.es650
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js.es6107
-rw-r--r--app/assets/javascripts/copy_as_gfm.js.es65
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es66
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es69
-rw-r--r--app/assets/javascripts/diff.js.es615
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es611
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es613
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es619
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es62
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es613
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es617
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es648
-rw-r--r--app/assets/javascripts/dispatcher.js.es652
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js4
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/due_date_select.js.es659
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es612
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js.es63
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js.es63
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js.es625
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.js.es63
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js.es63
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js.es63
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es68
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js.es61
-rw-r--r--app/assets/javascripts/environments/vue_resource_interceptor.js.es612
-rw-r--r--app/assets/javascripts/extensions/array.js.es65
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js.es613
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es611
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es612
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es64
-rw-r--r--app/assets/javascripts/gl_dropdown.js18
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es62
-rw-r--r--app/assets/javascripts/gl_form.js62
-rw-r--r--app/assets/javascripts/gl_form.js.es692
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js15
-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/groups_select.js4
-rw-r--r--app/assets/javascripts/issuable.js.es65
-rw-r--r--app/assets/javascripts/issuable/issuable_bundle.js.es62
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es63
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es62
-rw-r--r--app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es613
-rw-r--r--app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es67
-rw-r--r--app/assets/javascripts/issuable_form.js16
-rw-r--r--app/assets/javascripts/issue.js6
-rw-r--r--app/assets/javascripts/label_manager.js.es634
-rw-r--r--app/assets/javascripts/labels_select.js36
-rw-r--r--app/assets/javascripts/lib/ace/ace_config_paths.js.erb25
-rw-r--r--app/assets/javascripts/lib/chart.js6
-rw-r--r--app/assets/javascripts/lib/d3.js6
-rw-r--r--app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es61
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es638
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js101
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.es6126
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.erb6
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js3
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.es6 (renamed from app/assets/javascripts/lib/utils/url_utility.js)6
-rw-r--r--app/assets/javascripts/lib/vue_resource.js.es64
-rw-r--r--app/assets/javascripts/line_highlighter.js3
-rw-r--r--app/assets/javascripts/logo.js9
-rw-r--r--app/assets/javascripts/member_expiration_date.js.es633
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es616
-rw-r--r--app/assets/javascripts/merge_request.js22
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.es660
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es651
-rw-r--r--app/assets/javascripts/merge_request_widget/ci_bundle.js.es62
-rw-r--r--app/assets/javascripts/milestone.js160
-rw-r--r--app/assets/javascripts/milestone_select.js31
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js.es64
-rw-r--r--app/assets/javascripts/network/network_bundle.js10
-rw-r--r--app/assets/javascripts/notes.js22
-rw-r--r--app/assets/javascripts/pipelines.js.es62
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js10
-rw-r--r--app/assets/javascripts/project.js32
-rw-r--r--app/assets/javascripts/project_import.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es62
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js4
-rw-r--r--app/assets/javascripts/render_gfm.js2
-rw-r--r--app/assets/javascripts/search.js8
-rw-r--r--app/assets/javascripts/shortcuts.js3
-rw-r--r--app/assets/javascripts/shortcuts_blob.js29
-rw-r--r--app/assets/javascripts/shortcuts_blob.js.es629
-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.js16
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/shortcuts_network.js2
-rw-r--r--app/assets/javascripts/sidebar.js.es640
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/smart_interval.js.es65
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js4
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es62
-rw-r--r--app/assets/javascripts/terminal/terminal_bundle.js.es610
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js (renamed from app/assets/javascripts/boards/test_utils/simulate_drag.js)5
-rw-r--r--app/assets/javascripts/todos.js.es629
-rw-r--r--app/assets/javascripts/tree.js6
-rw-r--r--app/assets/javascripts/user_tabs.js.es61
-rw-r--r--app/assets/javascripts/users/calendar.js5
-rw-r--r--app/assets/javascripts/users/users_bundle.js10
-rw-r--r--app/assets/javascripts/users_select.js16
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js.es666
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es612
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js.es6110
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es62
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stages.js.es621
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es613
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es63
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js.es615
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js.es6 (renamed from app/assets/javascripts/vue_common_component/commit.js.es6)2
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js.es661
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6234
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js.es6 (renamed from app/assets/javascripts/vue_pagination/index.js.es6)6
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es623
-rw-r--r--app/assets/javascripts/wikis.js.es66
-rw-r--r--app/assets/javascripts/zen_mode.js10
-rw-r--r--app/assets/stylesheets/application.scss3
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/animations.scss36
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss10
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/calendar.scss55
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss129
-rw-r--r--app/assets/stylesheets/framework/files.scss43
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/framework/icons.scss6
-rw-r--r--app/assets/stylesheets/framework/jquery.scss57
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss1
-rw-r--r--app/assets/stylesheets/framework/nav.scss25
-rw-r--r--app/assets/stylesheets/framework/pagination.scss14
-rw-r--r--app/assets/stylesheets/framework/progress.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss15
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss4
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss7
-rw-r--r--app/assets/stylesheets/pages/boards.scss176
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss7
-rw-r--r--app/assets/stylesheets/pages/events.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss17
-rw-r--r--app/assets/stylesheets/pages/issues.scss6
-rw-r--r--app/assets/stylesheets/pages/labels.scss21
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss84
-rw-r--r--app/assets/stylesheets/pages/milestone.scss6
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss150
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb4
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/concerns/creates_commit.rb53
-rw-r--r--app/controllers/concerns/issuable_collections.rb22
-rw-r--r--app/controllers/concerns/issues_action.rb3
-rw-r--r--app/controllers/concerns/merge_requests_action.rb3
-rw-r--r--app/controllers/concerns/spammable_actions.rb20
-rw-r--r--app/controllers/dashboard/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb4
-rw-r--r--app/controllers/dashboard_controller.rb5
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb37
-rw-r--r--app/controllers/help_controller.rb2
-rw-r--r--app/controllers/koding_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb4
-rw-r--r--app/controllers/projects/blob_controller.rb10
-rw-r--r--app/controllers/projects/boards/issues_controller.rb6
-rw-r--r--app/controllers/projects/commit_controller.rb20
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/environments_controller.rb13
-rw-r--r--app/controllers/projects/git_http_client_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb29
-rw-r--r--app/controllers/projects/labels_controller.rb41
-rw-r--r--app/controllers/projects/lfs_api_controller.rb4
-rw-r--r--app/controllers/projects/mattermosts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb40
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pages_domains_controller.rb49
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb11
-rw-r--r--app/controllers/projects/protected_branches_controller.rb8
-rw-r--r--app/controllers/projects/runners_controller.rb8
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb44
-rw-r--r--app/controllers/projects/snippets_controller.rb8
-rw-r--r--app/controllers/projects/triggers_controller.rb10
-rw-r--r--app/controllers/projects/uploads_controller.rb6
-rw-r--r--app/controllers/projects/variables_controller.rb9
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/controllers/registrations_controller.rb6
-rw-r--r--app/controllers/root_controller.rb3
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb6
-rw-r--r--app/finders/environments_finder.rb55
-rw-r--r--app/finders/group_members_finder.rb20
-rw-r--r--app/finders/group_projects_finder.rb2
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/boards_helper.rb4
-rw-r--r--app/helpers/commits_helper.rb15
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/javascript_helper.rb5
-rw-r--r--app/helpers/merge_requests_helper.rb20
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/todos_helper.rb6
-rw-r--r--app/helpers/visibility_level_helper.rb4
-rw-r--r--app/mailers/notify.rb2
-rw-r--r--app/models/application_setting.rb35
-rw-r--r--app/models/award_emoji.rb8
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb63
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/commit.rb4
-rw-r--r--app/models/concerns/issuable.rb9
-rw-r--r--app/models/concerns/mentionable.rb19
-rw-r--r--app/models/concerns/milestoneish.rb2
-rw-r--r--app/models/concerns/routable.rb66
-rw-r--r--app/models/concerns/spammable.rb14
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/directly_addressed_user.rb7
-rw-r--r--app/models/environment.rb55
-rw-r--r--app/models/group.rb9
-rw-r--r--app/models/group_milestone.rb2
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/merge_request_diff.rb3
-rw-r--r--app/models/namespace.rb45
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/pages_domain.rb119
-rw-r--r--app/models/project.rb151
-rw-r--r--app/models/project_services/chat_slash_commands_service.rb16
-rw-r--r--app/models/project_services/jira_service.rb14
-rw-r--r--app/models/project_services/kubernetes_service.rb11
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb8
-rw-r--r--app/models/project_services/slack_slash_commands_service.rb4
-rw-r--r--app/models/project_snippet.rb4
-rw-r--r--app/models/repository.rb513
-rw-r--r--app/models/route.rb27
-rw-r--r--app/models/snippet.rb14
-rw-r--r--app/models/timelog.rb18
-rw-r--r--app/models/todo.rb20
-rw-r--r--app/models/user.rb33
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/policies/ci/build_policy.rb2
-rw-r--r--app/policies/project_policy.rb51
-rw-r--r--app/policies/project_snippet_policy.rb2
-rw-r--r--app/serializers/base_serializer.rb1
-rw-r--r--app/serializers/environment_entity.rb2
-rw-r--r--app/serializers/environment_serializer.rb47
-rw-r--r--app/serializers/pipeline_serializer.rb41
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/boards/create_service.rb1
-rw-r--r--app/services/boards/issues/list_service.rb14
-rw-r--r--app/services/ci/stop_environments_service.rb7
-rw-r--r--app/services/commits/change_service.rb34
-rw-r--r--app/services/compare_service.rb26
-rw-r--r--app/services/create_branch_service.rb30
-rw-r--r--app/services/create_snippet_service.rb9
-rw-r--r--app/services/create_tag_service.rb2
-rw-r--r--app/services/delete_tag_service.rb4
-rw-r--r--app/services/delete_user_service.rb31
-rw-r--r--app/services/destroy_group_service.rb29
-rw-r--r--app/services/files/base_service.rb25
-rw-r--r--app/services/files/create_dir_service.rb10
-rw-r--r--app/services/files/create_service.rb14
-rw-r--r--app/services/files/delete_service.rb7
-rw-r--r--app/services/files/destroy_service.rb15
-rw-r--r--app/services/files/multi_service.rb8
-rw-r--r--app/services/files/update_service.rb10
-rw-r--r--app/services/git_hooks_service.rb6
-rw-r--r--app/services/git_operation_service.rb179
-rw-r--r--app/services/groups/destroy_service.rb25
-rw-r--r--app/services/issues/build_service.rb10
-rw-r--r--app/services/issues/create_service.rb12
-rw-r--r--app/services/labels/promote_service.rb71
-rw-r--r--app/services/merge_requests/build_service.rb132
-rw-r--r--app/services/merge_requests/merge_service.rb14
-rw-r--r--app/services/merge_requests/refresh_service.rb6
-rw-r--r--app/services/notes/destroy_service.rb (renamed from app/services/notes/delete_service.rb)2
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/services/pages_service.rb15
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/transfer_service.rb20
-rw-r--r--app/services/projects/update_pages_configuration_service.rb69
-rw-r--r--app/services/projects/update_pages_service.rb164
-rw-r--r--app/services/search/global_service.rb5
-rw-r--r--app/services/slash_commands/interpret_service.rb12
-rw-r--r--app/services/spam_service.rb3
-rw-r--r--app/services/system_note_service.rb23
-rw-r--r--app/services/todo_service.rb18
-rw-r--r--app/services/users/destroy_service.rb33
-rw-r--r--app/services/validate_new_branch_service.rb22
-rw-r--r--app/validators/certificate_key_validator.rb25
-rw-r--r--app/validators/certificate_validator.rb24
-rw-r--r--app/views/admin/application_settings/_form.html.haml20
-rw-r--r--app/views/admin/builds/index.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/logs/show.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml13
-rw-r--r--app/views/admin/runners/index.html.haml10
-rw-r--r--app/views/admin/runners/show.html.haml8
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml2
-rw-r--r--app/views/admin/spam_logs/index.html.haml1
-rw-r--r--app/views/admin/users/_access_levels.html.haml37
-rw-r--r--app/views/admin/users/_form.html.haml23
-rw-r--r--app/views/ci/lints/show.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml2
-rw-r--r--app/views/ci/status/_graph_badge.html.haml6
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/dashboard/merge_requests.html.haml4
-rw-r--r--app/views/dashboard/todos/_todo.html.haml5
-rw-r--r--app/views/dashboard/todos/index.html.haml16
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/discussions/_resolve_all.html.haml3
-rw-r--r--app/views/groups/_home_panel.html.haml17
-rw-r--r--app/views/groups/_show_nav.html.haml7
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml4
-rw-r--r--app/views/groups/group_members/index.html.haml6
-rw-r--r--app/views/groups/issues.html.haml3
-rw-r--r--app/views/groups/merge_requests.html.haml3
-rw-r--r--app/views/groups/milestones/show.html.haml4
-rw-r--r--app/views/groups/show.html.haml43
-rw-r--r--app/views/groups/subgroups.html.haml20
-rw-r--r--app/views/help/_shortcuts.html.haml10
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml4
-rw-r--r--app/views/layouts/application.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml20
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/build_fail_email.html.haml4
-rw-r--r--app/views/notify/build_fail_email.text.erb2
-rw-r--r--app/views/notify/build_success_email.html.haml4
-rw-r--r--app/views/notify/build_success_email.text.erb2
-rw-r--r--app/views/notify/links/ci/builds/_build.text.erb2
-rw-r--r--app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb2
-rw-r--r--app/views/profiles/_head.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml16
-rw-r--r--app/views/projects/_customize_workflow.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml1
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml4
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_actions.html.haml5
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/blob/diff.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml12
-rw-r--r--app/views/projects/boards/components/_board.html.haml1
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml1
-rw-r--r--app/views/projects/boards/components/_card.html.haml26
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml2
-rw-r--r--app/views/projects/builds/_header.html.haml4
-rw-r--r--app/views/projects/builds/_sidebar.html.haml12
-rw-r--r--app/views/projects/builds/_table.html.haml4
-rw-r--r--app/views/projects/builds/index.html.haml4
-rw-r--r--app/views/projects/builds/show.html.haml20
-rw-r--r--app/views/projects/ci/builds/_build.html.haml4
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml25
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/commit/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_pipelines_list.haml40
-rw-r--r--app/views/projects/commit/pipelines.html.haml9
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml4
-rw-r--r--app/views/projects/compare/_form.html.haml4
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/diffs/_file.html.haml11
-rw-r--r--app/views/projects/diffs/_file_header.html.haml6
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml20
-rw-r--r--app/views/projects/diffs/_text_file.html.haml8
-rw-r--r--app/views/projects/edit.html.haml9
-rw-r--r--app/views/projects/environments/_stop.html.haml2
-rw-r--r--app/views/projects/environments/index.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml8
-rw-r--r--app/views/projects/environments/terminal.html.haml4
-rw-r--r--app/views/projects/graphs/_head.html.haml4
-rw-r--r--app/views/projects/graphs/ci/_builds.haml8
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_issue.html.haml96
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml12
-rw-r--r--app/views/projects/issues/verify.html.haml20
-rw-r--r--app/views/projects/labels/destroy.js.haml2
-rw-r--r--app/views/projects/labels/index.html.haml59
-rw-r--r--app/views/projects/mattermosts/_no_teams.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml126
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml16
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml6
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml18
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml12
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_build_failed.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml4
-rw-r--r--app/views/projects/milestones/show.html.haml3
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml7
-rw-r--r--app/views/projects/notes/_note.html.haml14
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/pages/_access.html.haml13
-rw-r--r--app/views/projects/pages/_destroy.haml12
-rw-r--r--app/views/projects/pages/_disabled.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml17
-rw-r--r--app/views/projects/pages/_no_domains.html.haml7
-rw-r--r--app/views/projects/pages/_use.html.haml8
-rw-r--r--app/views/projects/pages/show.html.haml26
-rw-r--r--app/views/projects/pages_domains/_form.html.haml34
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml30
-rw-r--r--app/views/projects/pipelines/_head.html.haml4
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml48
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml (renamed from app/views/projects/pipelines_settings/show.html.haml)8
-rw-r--r--app/views/projects/protected_branches/index.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml2
-rw-r--r--app/views/projects/runners/_index.html.haml (renamed from app/views/projects/runners/index.html.haml)14
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml30
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml17
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml32
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml6
-rw-r--r--app/views/projects/snippets/_actions.html.haml7
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/triggers/_index.html.haml (renamed from app/views/projects/triggers/index.html.haml)14
-rw-r--r--app/views/projects/variables/_content.html.haml2
-rw-r--r--app/views/projects/variables/_index.html.haml (renamed from app/views/projects/variables/index.html.haml)8
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml2
-rw-r--r--app/views/search/results/_wiki_blob.html.haml2
-rw-r--r--app/views/shared/_commit_message_container.html.haml4
-rw-r--r--app/views/shared/_group_form.html.haml7
-rw-r--r--app/views/shared/_import_form.html.haml4
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml19
-rw-r--r--app/views/shared/_issues.html.haml15
-rw-r--r--app/views/shared/_label.html.haml8
-rw-r--r--app/views/shared/_merge_requests.html.haml14
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml18
-rw-r--r--app/views/shared/_outdated_browser.html.haml3
-rw-r--r--app/views/shared/_visibility_level.html.haml11
-rw-r--r--app/views/shared/empty_states/_labels.html.haml11
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml3
-rw-r--r--app/views/shared/empty_states/icons/_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_priority_labels.svg1
-rw-r--r--app/views/shared/empty_states/icons/_todos_all_done.svg (renamed from app/views/shared/empty_states/_todos_all_done.svg)0
-rw-r--r--app/views/shared/empty_states/icons/_todos_empty.svg (renamed from app/views/shared/empty_states/_todos_empty.svg)0
-rw-r--r--app/views/shared/groups/_group.html.haml6
-rw-r--r--app/views/shared/icons/_icon_action_cancel.svg1
-rw-r--r--app/views/shared/icons/_icon_action_play.svg1
-rw-r--r--app/views/shared/icons/_icon_action_retry.svg1
-rw-r--r--app/views/shared/icons/_icon_action_stop.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml15
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml7
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml9
-rw-r--r--app/views/shared/issuable/form/_merge_params.html.haml16
-rw-r--r--app/views/shared/members/_member.html.haml7
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml3
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/projects/_dropdown.html.haml14
-rw-r--r--app/views/shared/snippets/_form.html.haml7
-rw-r--r--app/views/shared/web_hooks/_form.html.haml6
-rw-r--r--app/views/sherlock/file_samples/show.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml45
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--app/workers/authorized_projects_worker.rb2
-rw-r--r--app/workers/delete_user_worker.rb2
-rw-r--r--app/workers/emails_on_push_worker.rb6
-rw-r--r--app/workers/group_destroy_worker.rb2
-rw-r--r--app/workers/pages_worker.rb23
-rwxr-xr-xbin/teaspoon8
-rw-r--r--changelogs/unreleased/1051-api-create-users-without-password.yml4
-rw-r--r--changelogs/unreleased/17662-rename-builds.yml4
-rw-r--r--changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml4
-rw-r--r--changelogs/unreleased/20495-plus-icon-button.yml4
-rw-r--r--changelogs/unreleased/20732_member_exists_409.yml4
-rw-r--r--changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml4
-rw-r--r--changelogs/unreleased/21518_recaptcha_spam_issues.yml4
-rw-r--r--changelogs/unreleased/22007-unify-projects-search.yml4
-rw-r--r--changelogs/unreleased/23104-remove-public-param-for-projects.yml4
-rw-r--r--changelogs/unreleased/23634-remove-project-grouping.yml4
-rw-r--r--changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml4
-rw-r--r--changelogs/unreleased/24147-delete-env-button.yml4
-rw-r--r--changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml4
-rw-r--r--changelogs/unreleased/24606-force-password-reset-on-next-login.yml4
-rw-r--r--changelogs/unreleased/24716-fix-ctrl-click-links.yml4
-rw-r--r--changelogs/unreleased/24795_refactor_merge_request_build_service.yml4
-rw-r--r--changelogs/unreleased/24976-start-of-line-mention.yml4
-rw-r--r--changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml4
-rw-r--r--changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml4
-rw-r--r--changelogs/unreleased/25460-replace-word-users-with-members.yml4
-rw-r--r--changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml4
-rw-r--r--changelogs/unreleased/25709-diff-file-overflow.yml4
-rw-r--r--changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml4
-rw-r--r--changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml4
-rw-r--r--changelogs/unreleased/26059-segoe-ui-vertical.yml4
-rw-r--r--changelogs/unreleased/26705-filter-todos-by-manual-add.yml4
-rw-r--r--changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml4
-rw-r--r--changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml4
-rw-r--r--changelogs/unreleased/26852-fix-slug-for-openshift.yml4
-rw-r--r--changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml4
-rw-r--r--changelogs/unreleased/26908-make-timelogs-use-foreign-keys4
-rw-r--r--changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml4
-rw-r--r--changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml4
-rw-r--r--changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml4
-rw-r--r--changelogs/unreleased/27240-make-progress-bars-consistent.yml4
-rw-r--r--changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml4
-rw-r--r--changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml4
-rw-r--r--changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml4
-rw-r--r--changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml4
-rw-r--r--changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml4
-rw-r--r--changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml4
-rw-r--r--changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml4
-rw-r--r--changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml5
-rw-r--r--changelogs/unreleased/27352-search-label-filter-header.yml4
-rw-r--r--changelogs/unreleased/27484-environment-show-name.yml4
-rw-r--r--changelogs/unreleased/27488-fix-jwt-version.yml4
-rw-r--r--changelogs/unreleased/27494-environment-list-column-headers.yml4
-rw-r--r--changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml4
-rw-r--r--changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml4
-rw-r--r--changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml4
-rw-r--r--changelogs/unreleased/27610-issue-number-alignment.yml4
-rw-r--r--changelogs/unreleased/27632_fix_mr_widget_url.yml4
-rw-r--r--changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml4
-rw-r--r--changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml4
-rw-r--r--changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml4
-rw-r--r--changelogs/unreleased/27783-fix-fe-doc-broken-link.yml4
-rw-r--r--changelogs/unreleased/27822-default-bulk-assign-labels.yml4
-rw-r--r--changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml4
-rw-r--r--changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml4
-rw-r--r--changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml4
-rw-r--r--changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml4
-rw-r--r--changelogs/unreleased/27963-tooltips-jobs.yml4
-rw-r--r--changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml4
-rw-r--r--changelogs/unreleased/27991-success-with-warnings-caret.yml4
-rw-r--r--changelogs/unreleased/27994-fix-mr-widget-jump.yml4
-rw-r--r--changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml4
-rw-r--r--changelogs/unreleased/28032-tooltips-file-name.yml5
-rw-r--r--changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml4
-rw-r--r--changelogs/unreleased/8082-permalink-to-file.yml4
-rw-r--r--changelogs/unreleased/9-0-api-changes.yml4
-rw-r--r--changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml4
-rw-r--r--changelogs/unreleased/api-fix-files.yml4
-rw-r--r--changelogs/unreleased/api-remove-snippets-expires-at.yml4
-rw-r--r--changelogs/unreleased/babel-all-the-things.yml5
-rw-r--r--changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml4
-rw-r--r--changelogs/unreleased/cop-gem-fetcher.yml4
-rw-r--r--changelogs/unreleased/copy-branch-to-clipboard.yml4
-rw-r--r--changelogs/unreleased/document-how-to-vue.yml4
-rw-r--r--changelogs/unreleased/dont-delete-assigned-issuables.yml4
-rw-r--r--changelogs/unreleased/dynamic-header-fixture.yml4
-rw-r--r--changelogs/unreleased/dz-create-nested-groups-via-ui.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-api.yml4
-rw-r--r--changelogs/unreleased/dz-nested-groups-improvements-2.yml4
-rw-r--r--changelogs/unreleased/dz-refactor-full-path.yml4
-rw-r--r--changelogs/unreleased/empty-selection-reply-shortcut.yml4
-rw-r--r--changelogs/unreleased/fe-commit-mr-pipelines.yml4
-rw-r--r--changelogs/unreleased/fix-26518.yml4
-rw-r--r--changelogs/unreleased/fix-27479.yml4
-rw-r--r--changelogs/unreleased/fix-anchor-scrolling.yml4
-rw-r--r--changelogs/unreleased/fix-cancel-integration-settings.yml4
-rw-r--r--changelogs/unreleased/fix-ci-build-policy.yml4
-rw-r--r--changelogs/unreleased/fix-cycle-analytics-events-limit.yml4
-rw-r--r--changelogs/unreleased/fix-deleting-project-again.yml4
-rw-r--r--changelogs/unreleased/fix-depr-warn.yml4
-rw-r--r--changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml4
-rw-r--r--changelogs/unreleased/fix-import-encrypt-atts.yml4
-rw-r--r--changelogs/unreleased/fix-import-group-members.yml4
-rw-r--r--changelogs/unreleased/fix-job-to-pipeline-renaming.yml4
-rw-r--r--changelogs/unreleased/fix-references-header-parsing.yml5
-rw-r--r--changelogs/unreleased/fix-scroll-test.yml4
-rw-r--r--changelogs/unreleased/fix-search-bar-search-param.yml4
-rw-r--r--changelogs/unreleased/fix_issue_from_milestone.yml4
-rw-r--r--changelogs/unreleased/fixes-namespace-api-documentation.yml4
-rw-r--r--changelogs/unreleased/fwn-to-find-by-full-path.yml4
-rw-r--r--changelogs/unreleased/git_to_html_redirection.yml4
-rw-r--r--changelogs/unreleased/go-go-gadget-webpack.yml4
-rw-r--r--changelogs/unreleased/group-label-sidebar-link.yml4
-rw-r--r--changelogs/unreleased/hardcode-title-system-note.yml4
-rw-r--r--changelogs/unreleased/improve-ci-example-php-doc.yml4
-rw-r--r--changelogs/unreleased/improve-handleLocationHash-tests.yml4
-rw-r--r--changelogs/unreleased/issue-20428.yml4
-rw-r--r--changelogs/unreleased/issue-newproj-layout.yml4
-rw-r--r--changelogs/unreleased/issue-sidebar-empty-assignee.yml4
-rw-r--r--changelogs/unreleased/issue_19262.yml4
-rw-r--r--changelogs/unreleased/issue_25900.yml4
-rw-r--r--changelogs/unreleased/issue_26701.yml4
-rw-r--r--changelogs/unreleased/jej-pages-picked-from-ee.yml4
-rw-r--r--changelogs/unreleased/label-promotion.yml4
-rw-r--r--changelogs/unreleased/label-select-toggle.yml4
-rw-r--r--changelogs/unreleased/lfs-noauth-public-repo.yml4
-rw-r--r--changelogs/unreleased/markdown-plantuml.yml4
-rw-r--r--changelogs/unreleased/new-branch-fixture.yml4
-rw-r--r--changelogs/unreleased/no-sidebar-on-action-btn-click.yml4
-rw-r--r--changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml4
-rw-r--r--changelogs/unreleased/pms-lowercase-system-notes.yml4
-rw-r--r--changelogs/unreleased/protected-branch-dropdown-titles.yml4
-rw-r--r--changelogs/unreleased/redesign-searchbar-admin-project-26794.yml4
-rw-r--r--changelogs/unreleased/refresh-authorizations-fork-join.yml4
-rw-r--r--changelogs/unreleased/refresh-permissions-when-moving-projects.yml4
-rw-r--r--changelogs/unreleased/removal_of_unused_parameter.yml4
-rw-r--r--changelogs/unreleased/remove-deploy-key-endpoint.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-datepicker.yml4
-rw-r--r--changelogs/unreleased/remove-jquery-ui-sortable.yml4
-rw-r--r--changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml4
-rw-r--r--changelogs/unreleased/rename_delete_services.yml4
-rw-r--r--changelogs/unreleased/rename_files_delete_service.yml4
-rw-r--r--changelogs/unreleased/revert-filter-assigned-to-me.yml4
-rw-r--r--changelogs/unreleased/route-map.yml4
-rw-r--r--changelogs/unreleased/rs-warden-blocked-users.yml4
-rw-r--r--changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml4
-rw-r--r--changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml4
-rw-r--r--changelogs/unreleased/slash-commands-typo.yml4
-rw-r--r--changelogs/unreleased/static-navbar.yml4
-rw-r--r--changelogs/unreleased/terminal-max-session-time.yml4
-rw-r--r--changelogs/unreleased/upgrade-babel-v6.yml4
-rw-r--r--changelogs/unreleased/upgrade-webpack-v2-2.yml4
-rw-r--r--changelogs/unreleased/zj-format-chat-messages.yml4
-rw-r--r--config/application.rb32
-rw-r--r--config/dependency_decisions.yml139
-rw-r--r--config/gitlab.yml.example25
-rw-r--r--config/initializers/1_settings.rb47
-rw-r--r--config/initializers/plantuml_lexer.rb2
-rw-r--r--config/initializers/request_profiler.rb2
-rw-r--r--config/initializers/rspec_profiling.rb8
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/initializers/static_files.rb31
-rw-r--r--config/karma.config.js21
-rw-r--r--config/routes/group.rb1
-rw-r--r--config/routes/project.rb9
-rw-r--r--config/routes/snippets.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js125
-rw-r--r--db/fixtures/development/10_merge_requests.rb2
-rw-r--r--db/migrate/20151215132013_add_pages_size_to_application_settings.rb14
-rw-r--r--db/migrate/20160210105555_create_pages_domain.rb16
-rw-r--r--db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb8
-rw-r--r--db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb6
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb2
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb2
-rw-r--r--db/migrate/20161031171301_add_project_id_to_subscriptions.rb2
-rw-r--r--db/migrate/20161114024742_add_coverage_regex_to_builds.rb13
-rw-r--r--db/migrate/20161207231626_add_environment_slug.rb18
-rw-r--r--db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb54
-rw-r--r--db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb33
-rw-r--r--db/migrate/20170127032550_remove_backlog_lists_from_boards.rb17
-rw-r--r--db/migrate/20170130204620_add_index_to_project_authorizations.rb11
-rw-r--r--db/migrate/20170204172458_add_name_to_route.rb12
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb11
-rw-r--r--db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb15
-rw-r--r--db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb11
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb23
-rw-r--r--db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb32
-rw-r--r--db/schema.rb37
-rw-r--r--doc/README.md8
-rw-r--r--doc/administration/container_registry.md4
-rw-r--r--doc/administration/custom_hooks.md3
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/high_availability/nfs.md18
-rw-r--r--doc/administration/integration/plantuml.md18
-rw-r--r--doc/administration/integration/terminal.md26
-rw-r--r--doc/administration/monitoring/performance/introduction.md2
-rw-r--r--doc/administration/monitoring/performance/prometheus.md103
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/index.md147
-rw-r--r--doc/administration/monitoring/prometheus/node_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/postgres_exporter.md30
-rw-r--r--doc/administration/monitoring/prometheus/redis_exporter.md33
-rw-r--r--doc/administration/pages/index.md249
-rw-r--r--doc/administration/pages/source.md323
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md2
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/groups.md67
-rw-r--r--doc/api/issues.md1
-rw-r--r--doc/api/merge_requests.md12
-rw-r--r--doc/api/oauth2.md6
-rw-r--r--doc/api/project_snippets.md1
-rw-r--r--doc/api/projects.md48
-rw-r--r--doc/api/repository_files.md8
-rw-r--r--doc/api/services.md2
-rw-r--r--doc/api/settings.md7
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/api/v3_to_v4.md15
-rw-r--r--doc/ci/autodeploy/index.md7
-rw-r--r--doc/ci/environments.md66
-rw-r--r--doc/ci/examples/php.md6
-rw-r--r--doc/ci/img/view_on_env_blob.pngbin0 -> 111663 bytes
-rw-r--r--doc/ci/img/view_on_env_mr.pngbin0 -> 1005195 bytes
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/quick_start/README.md2
-rw-r--r--doc/ci/variables/README.md4
-rw-r--r--doc/ci/yaml/README.md47
-rw-r--r--doc/development/frontend.md188
-rw-r--r--doc/development/rake_tasks.md10
-rw-r--r--doc/development/testing.md7
-rw-r--r--doc/development/ui_guide.md6
-rw-r--r--doc/development/ux_guide/animation.md2
-rw-r--r--doc/development/ux_guide/components.md14
-rw-r--r--doc/gitlab-basics/command-line-commands.md2
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin3045 -> 5842 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin8133 -> 24639 bytes
-rw-r--r--doc/install/README.md3
-rw-r--r--doc/install/digitaloceandocker.md136
-rw-r--r--doc/install/installation.md13
-rw-r--r--doc/integration/README.md11
-rw-r--r--doc/integration/auth0.md7
-rw-r--r--doc/integration/azure.md6
-rw-r--r--doc/integration/cas.md9
-rw-r--r--doc/integration/crowd.md9
-rw-r--r--doc/integration/external-issue-tracker.md8
-rw-r--r--doc/integration/facebook.md6
-rw-r--r--doc/integration/github.md22
-rw-r--r--doc/integration/gitlab.md16
-rw-r--r--doc/integration/google.md6
-rw-r--r--doc/integration/jira.md4
-rw-r--r--doc/integration/saml.md6
-rw-r--r--doc/integration/shibboleth.md12
-rw-r--r--doc/integration/twitter.md6
-rw-r--r--doc/pages/README.md1
-rw-r--r--doc/pages/administration.md1
-rw-r--r--doc/project_services/bamboo.md61
-rw-r--r--doc/project_services/bugzilla.md18
-rw-r--r--doc/project_services/builds_emails.md17
-rw-r--r--doc/project_services/emails_on_push.md18
-rw-r--r--doc/project_services/hipchat.md55
-rw-r--r--doc/project_services/img/builds_emails_service.pngbin19203 -> 0 bytes
-rw-r--r--doc/project_services/img/mattermost_config_help.pngbin63138 -> 0 bytes
-rw-r--r--doc/project_services/img/slack_setup.pngbin126412 -> 0 bytes
-rw-r--r--doc/project_services/irker.md52
-rw-r--r--doc/project_services/jira.md209
-rw-r--r--doc/project_services/kubernetes.md64
-rw-r--r--doc/project_services/mattermost.md46
-rw-r--r--doc/project_services/mattermost_slash_commands.md164
-rw-r--r--doc/project_services/project_services.md60
-rw-r--r--doc/project_services/redmine.md22
-rw-r--r--doc/project_services/services_templates.md26
-rw-r--r--doc/project_services/slack.md51
-rw-r--r--doc/project_services/slack_slash_commands.md24
-rw-r--r--doc/raketasks/backup_restore.md23
-rw-r--r--doc/raketasks/features.md2
-rw-r--r--doc/security/webhooks.md4
-rw-r--r--doc/ssh/README.md10
-rw-r--r--doc/university/README.md8
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/university/support/README.md2
-rwxr-xr-xdoc/university/training/topics/additional_resources.md2
-rwxr-xr-xdoc/university/training/user_training.md2
-rw-r--r--doc/update/2.6-to-3.0.md2
-rw-r--r--doc/update/2.9-to-3.0.md2
-rw-r--r--doc/update/3.0-to-3.1.md2
-rw-r--r--doc/update/3.1-to-4.0.md2
-rw-r--r--doc/update/4.0-to-4.1.md2
-rw-r--r--doc/update/4.1-to-4.2.md2
-rw-r--r--doc/update/4.2-to-5.0.md2
-rw-r--r--doc/update/5.0-to-5.1.md2
-rw-r--r--doc/update/5.1-to-5.2.md2
-rw-r--r--doc/update/5.1-to-5.4.md2
-rw-r--r--doc/update/5.1-to-6.0.md2
-rw-r--r--doc/update/5.2-to-5.3.md2
-rw-r--r--doc/update/5.3-to-5.4.md2
-rw-r--r--doc/update/5.4-to-6.0.md2
-rw-r--r--doc/update/6.0-to-6.1.md2
-rw-r--r--doc/update/6.1-to-6.2.md2
-rw-r--r--doc/update/6.2-to-6.3.md2
-rw-r--r--doc/update/6.3-to-6.4.md2
-rw-r--r--doc/update/6.4-to-6.5.md2
-rw-r--r--doc/update/6.5-to-6.6.md2
-rw-r--r--doc/update/6.6-to-6.7.md2
-rw-r--r--doc/update/6.7-to-6.8.md2
-rw-r--r--doc/update/6.8-to-6.9.md2
-rw-r--r--doc/update/6.9-to-7.0.md2
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md4
-rw-r--r--doc/update/7.0-to-7.1.md2
-rw-r--r--doc/update/7.1-to-7.2.md2
-rw-r--r--doc/update/7.2-to-7.3.md2
-rw-r--r--doc/update/7.3-to-7.4.md2
-rw-r--r--doc/update/8.16-to-8.17.md239
-rw-r--r--doc/user/account/security.md2
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md24
-rw-r--r--doc/user/markdown.md35
-rw-r--r--doc/user/permissions.md3
-rw-r--r--doc/user/profile/account/two_factor_authentication.md2
-rw-r--r--doc/user/project/container_registry.md11
-rw-r--r--doc/user/project/img/issue_board.pngbin90664 -> 76461 bytes
-rw-r--r--doc/user/project/img/issue_board_search_backlog.pngbin9769 -> 0 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin97419 -> 120751 bytes
-rw-r--r--doc/user/project/img/issue_boards_add_issues_modal.pngbin0 -> 177057 bytes
-rw-r--r--doc/user/project/img/issue_boards_remove_issue.pngbin0 -> 135168 bytes
-rw-r--r--doc/user/project/img/protected_branches_devs_can_push.pngbin8302 -> 34888 bytes
-rw-r--r--doc/user/project/integrations/bamboo.md59
-rw-r--r--doc/user/project/integrations/bugzilla.md18
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/emails_on_push.md20
-rw-r--r--doc/user/project/integrations/hipchat.md53
-rw-r--r--doc/user/project/integrations/img/accessing_integrations.pngbin0 -> 8941 bytes
-rw-r--r--doc/user/project/integrations/img/emails_on_push_service.png (renamed from doc/project_services/img/emails_on_push_service.png)bin28535 -> 28535 bytes
-rw-r--r--doc/user/project/integrations/img/jira_add_user_to_group.png (renamed from doc/project_services/img/jira_add_user_to_group.png)bin24838 -> 24838 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group.png (renamed from doc/project_services/img/jira_create_new_group.png)bin19127 -> 19127 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_group_name.png (renamed from doc/project_services/img/jira_create_new_group_name.png)bin5168 -> 5168 bytes
-rw-r--r--doc/user/project/integrations/img/jira_create_new_user.png (renamed from doc/project_services/img/jira_create_new_user.png)bin12625 -> 12625 bytes
-rw-r--r--doc/user/project/integrations/img/jira_group_access.png (renamed from doc/project_services/img/jira_group_access.png)bin19235 -> 19235 bytes
-rw-r--r--doc/user/project/integrations/img/jira_issue_reference.png (renamed from doc/project_services/img/jira_issue_reference.png)bin18399 -> 18399 bytes
-rw-r--r--doc/user/project/integrations/img/jira_merge_request_close.png (renamed from doc/project_services/img/jira_merge_request_close.png)bin21172 -> 21172 bytes
-rw-r--r--doc/user/project/integrations/img/jira_project_name.png (renamed from doc/project_services/img/jira_project_name.png)bin26685 -> 26685 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service.png (renamed from doc/project_services/img/jira_service.png)bin37869 -> 37869 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_comment.png (renamed from doc/project_services/img/jira_service_close_comment.png)bin11893 -> 11893 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_close_issue.png (renamed from doc/project_services/img/jira_service_close_issue.png)bin30570 -> 30570 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service_page.png (renamed from doc/project_services/img/jira_service_page.png)bin12228 -> 12228 bytes
-rw-r--r--doc/user/project/integrations/img/jira_user_management_link.png (renamed from doc/project_services/img/jira_user_management_link.png)bin23921 -> 23921 bytes
-rw-r--r--doc/user/project/integrations/img/jira_workflow_screenshot.png (renamed from doc/project_services/img/jira_workflow_screenshot.png)bin66685 -> 66685 bytes
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.png (renamed from doc/project_services/img/kubernetes_configuration.png)bin113827 -> 113827 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_add_slash_command.png (renamed from doc/project_services/img/mattermost_add_slash_command.png)bin9265 -> 9265 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_auth.png (renamed from doc/project_services/img/mattermost_bot_auth.png)bin8676 -> 8676 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_bot_available_commands.png (renamed from doc/project_services/img/mattermost_bot_available_commands.png)bin4647 -> 4647 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_config_help.pngbin0 -> 102890 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.png (renamed from doc/project_services/img/mattermost_configuration.png)bin73502 -> 73502 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_console_integrations.png (renamed from doc/project_services/img/mattermost_console_integrations.png)bin314642 -> 314642 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_gitlab_token.png (renamed from doc/project_services/img/mattermost_gitlab_token.png)bin3688 -> 3688 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_goto_console.png (renamed from doc/project_services/img/mattermost_goto_console.png)bin7754 -> 7754 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_configuration.png (renamed from doc/project_services/img/mattermost_slash_command_configuration.png)bin24169 -> 24169 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_slash_command_token.png (renamed from doc/project_services/img/mattermost_slash_command_token.png)bin8624 -> 8624 bytes
-rw-r--r--doc/user/project/integrations/img/mattermost_team_integrations.png (renamed from doc/project_services/img/mattermost_team_integrations.png)bin4766 -> 4766 bytes
-rw-r--r--doc/user/project/integrations/img/project_services.pngbin0 -> 25753 bytes
-rw-r--r--doc/user/project/integrations/img/redmine_configuration.png (renamed from doc/project_services/img/redmine_configuration.png)bin10266 -> 10266 bytes
-rw-r--r--doc/user/project/integrations/img/services_templates_redmine_example.png (renamed from doc/project_services/img/services_templates_redmine_example.png)bin8776 -> 8776 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.png (renamed from doc/project_services/img/slack_configuration.png)bin29825 -> 29825 bytes
-rw-r--r--doc/user/project/integrations/img/slack_setup.pngbin0 -> 86314 bytes
-rw-r--r--doc/user/project/integrations/img/webhooks_ssl.png (renamed from doc/web_hooks/ssl.png)bin27799 -> 27799 bytes
-rw-r--r--doc/user/project/integrations/index.md26
-rw-r--r--doc/user/project/integrations/irker.md50
-rw-r--r--doc/user/project/integrations/jira.md209
-rw-r--r--doc/user/project/integrations/kubernetes.md66
-rw-r--r--doc/user/project/integrations/mattermost.md46
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md164
-rw-r--r--doc/user/project/integrations/project_services.md76
-rw-r--r--doc/user/project/integrations/redmine.md23
-rw-r--r--doc/user/project/integrations/services_templates.md25
-rw-r--r--doc/user/project/integrations/slack.md51
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md24
-rw-r--r--doc/user/project/integrations/webhooks.md1028
-rw-r--r--doc/user/project/issue_board.md51
-rw-r--r--doc/user/project/merge_requests.md170
-rw-r--r--doc/user/project/merge_requests/index.md169
-rw-r--r--doc/user/project/merge_requests/versions.md17
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin0 -> 33597 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin0 -> 87071 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin0 -> 34686 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin0 -> 63716 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin0 -> 51136 bytes
-rw-r--r--doc/user/project/pages/img/pages_remove.pngbin0 -> 27259 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin0 -> 103730 bytes
-rw-r--r--doc/user/project/pages/index.md435
-rw-r--r--doc/user/project/repository/web_editor.md3
-rw-r--r--doc/user/project/settings/import_export.md11
-rw-r--r--doc/user/project/slash_commands.md3
-rw-r--r--doc/web_hooks/web_hooks.md1026
-rw-r--r--doc/workflow/README.md2
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/importing/import_projects_from_github.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--features/dashboard/shortcuts.feature21
-rw-r--r--features/project/active_tab.feature7
-rw-r--r--features/project/pages.feature82
-rw-r--r--features/snippets/user.feature34
-rw-r--r--features/steps/dashboard/shortcuts.rb7
-rw-r--r--features/steps/dashboard/todos.rb45
-rw-r--r--features/steps/project/active_tab.rb8
-rw-r--r--features/steps/project/builds/summary.rb2
-rw-r--r--features/steps/project/graph.rb6
-rw-r--r--features/steps/project/issues/labels.rb9
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--features/steps/project/pages.rb139
-rw-r--r--features/steps/shared/builds.rb4
-rw-r--r--features/steps/shared/project.rb6
-rw-r--r--features/steps/snippets/user.rb55
-rw-r--r--lib/api/api.rb12
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/builds.rb2
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/deploy_keys.rb172
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/groups.rb3
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/members.rb13
-rw-r--r--lib/api/merge_requests.rb274
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb35
-rw-r--r--lib/api/services.rb13
-rw-r--r--lib/api/settings.rb6
-rw-r--r--lib/api/snippets.rb2
-rw-r--r--lib/api/users.rb20
-rw-r--r--lib/api/v3/deploy_keys.rb122
-rw-r--r--lib/api/v3/entities.rb16
-rw-r--r--lib/api/v3/issues.rb231
-rw-r--r--lib/api/v3/members.rb134
-rw-r--r--lib/api/v3/merge_requests.rb280
-rw-r--r--lib/api/v3/project_snippets.rb135
-rw-r--r--lib/api/v3/projects.rb458
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/backup/pages.rb13
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter/plantuml_filter.rb39
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/querying.rb56
-rw-r--r--lib/banzai/reference_extractor.rb5
-rw-r--r--lib/banzai/reference_parser/base_parser.rb5
-rw-r--r--lib/banzai/reference_parser/directly_addressed_user_parser.rb8
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb1
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/gitlab/auth.rb11
-rw-r--r--lib/gitlab/chat_commands/base_command.rb4
-rw-r--r--lib/gitlab/chat_commands/command.rb35
-rw-r--r--lib/gitlab/chat_commands/deploy.rb24
-rw-r--r--lib/gitlab/chat_commands/help.rb28
-rw-r--r--lib/gitlab/chat_commands/issue_new.rb (renamed from lib/gitlab/chat_commands/issue_create.rb)20
-rw-r--r--lib/gitlab/chat_commands/issue_search.rb8
-rw-r--r--lib/gitlab/chat_commands/issue_show.rb8
-rw-r--r--lib/gitlab/chat_commands/presenter.rb131
-rw-r--r--lib/gitlab/chat_commands/presenters/access.rb40
-rw-r--r--lib/gitlab/chat_commands/presenters/base.rb77
-rw-r--r--lib/gitlab/chat_commands/presenters/deploy.rb21
-rw-r--r--lib/gitlab/chat_commands/presenters/help.rb27
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_base.rb43
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_new.rb50
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_search.rb47
-rw-r--r--lib/gitlab/chat_commands/presenters/issue_show.rb61
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb22
-rw-r--r--lib/gitlab/ci/config/entry/job.rb8
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb10
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb10
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb45
-rw-r--r--lib/gitlab/ci/status/build/cancelable.rb2
-rw-r--r--lib/gitlab/ci/status/build/play.rb6
-rw-r--r--lib/gitlab/ci/status/build/retryable.rb2
-rw-r--r--lib/gitlab/ci/status/build/stop.rb2
-rw-r--r--lib/gitlab/ci/status/core.rb3
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb4
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb2
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb2
-rw-r--r--lib/gitlab/cycle_analytics/staging_event_fetcher.rb2
-rw-r--r--lib/gitlab/database.rb14
-rw-r--r--lib/gitlab/database/migration_helpers.rb50
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb2
-rw-r--r--lib/gitlab/email/message/repository_push.rb4
-rw-r--r--lib/gitlab/email/receiver.rb21
-rw-r--r--lib/gitlab/git.rb2
-rw-r--r--lib/gitlab/git/blob_snippet.rb2
-rw-r--r--lib/gitlab/git_post_receive.rb4
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_export/members_mapper.rb6
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb27
-rw-r--r--lib/gitlab/import_export/reader.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb26
-rw-r--r--lib/gitlab/incoming_email.rb12
-rw-r--r--lib/gitlab/kubernetes.rb4
-rw-r--r--lib/gitlab/metrics.rb6
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb24
-rw-r--r--lib/gitlab/pages_transfer.rb7
-rw-r--r--lib/gitlab/project_transfer.rb35
-rw-r--r--lib/gitlab/recaptcha.rb4
-rw-r--r--lib/gitlab/reference_extractor.rb8
-rw-r--r--lib/gitlab/request_profiler/middleware.rb3
-rw-r--r--lib/gitlab/route_map.rb50
-rw-r--r--lib/gitlab/serializer/ci/variables.rb (renamed from lib/gitlab/serialize/ci/variables.rb)2
-rw-r--r--lib/gitlab/serializer/pagination.rb36
-rw-r--r--lib/gitlab/uploads_transfer.rb30
-rw-r--r--lib/gitlab/visibility_level.rb14
-rw-r--r--lib/gitlab/workhorse.rb3
-rw-r--r--lib/rouge/lexers/plantuml.rb21
-rwxr-xr-xlib/support/init.d/gitlab68
-rw-r--r--[-rwxr-xr-x]lib/support/init.d/gitlab.default.example24
-rw-r--r--lib/support/nginx/gitlab-pages28
-rw-r--r--lib/support/nginx/gitlab-pages-ssl77
-rw-r--r--lib/tasks/config_lint.rake25
-rw-r--r--lib/tasks/gitlab/assets.rake1
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/import.rake4
-rw-r--r--lib/tasks/gitlab/sidekiq.rake2
-rw-r--r--lib/tasks/gitlab/test.rake2
-rw-r--r--lib/tasks/grape.rake6
-rw-r--r--lib/tasks/karma.rake25
-rw-r--r--lib/tasks/teaspoon.rake25
-rw-r--r--lib/tasks/test.rake2
-rw-r--r--package.json41
-rw-r--r--rubocop/cop/gem_fetcher.rb37
-rw-r--r--rubocop/cop/migration/add_column.rb (renamed from rubocop/cop/migration/column_with_default.rb)8
-rw-r--r--rubocop/cop/migration/add_column_with_default.rb34
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb27
-rw-r--r--rubocop/cop/migration/add_index.rb4
-rw-r--r--rubocop/rubocop.rb6
-rw-r--r--shared/pages/.gitkeep0
-rw-r--r--spec/controllers/dashboard_controller_spec.rb19
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb76
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb24
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb91
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb45
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb82
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb64
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb20
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb84
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb3
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb64
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb59
-rw-r--r--spec/controllers/projects_controller_spec.rb11
-rw-r--r--spec/controllers/registrations_controller_spec.rb2
-rw-r--r--spec/controllers/search_controller_spec.rb16
-rw-r--r--spec/controllers/snippets_controller_spec.rb59
-rw-r--r--spec/factories/boards.rb1
-rw-r--r--spec/factories/events.rb12
-rw-r--r--spec/factories/lists.rb6
-rw-r--r--spec/factories/pages_domains.rb153
-rw-r--r--spec/factories/projects.rb55
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/factories/todos.rb8
-rw-r--r--spec/factories/users.rb8
-rw-r--r--spec/features/admin/admin_builds_spec.rb34
-rw-r--r--spec/features/admin/admin_labels_spec.rb11
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb233
-rw-r--r--spec/features/boards/boards_spec.rb233
-rw-r--r--spec/features/boards/modal_filter_spec.rb259
-rw-r--r--spec/features/boards/new_issue_spec.rb5
-rw-r--r--spec/features/boards/sidebar_spec.rb111
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb29
-rw-r--r--spec/features/environment_spec.rb68
-rw-r--r--spec/features/environments_spec.rb18
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb28
-rw-r--r--spec/features/groups/members/list_spec.rb40
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups_spec.rb19
-rw-r--r--spec/features/issuables/issuable_list_spec.rb57
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb218
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb22
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb15
-rw-r--r--spec/features/issues/group_label_sidebar_spec.rb21
-rw-r--r--spec/features/issues/new_branch_button_spec.rb20
-rw-r--r--spec/features/issues/spam_issues_spec.rb66
-rw-r--r--spec/features/issues_spec.rb18
-rw-r--r--spec/features/login_spec.rb21
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb2
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb20
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb100
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb4
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb28
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb78
-rw-r--r--spec/features/merge_requests/widget_spec.rb55
-rw-r--r--spec/features/milestones/milestones_spec.rb5
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb6
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb2
-rw-r--r--spec/features/projects/blobs/shortcuts_blob_spec.rb37
-rw-r--r--spec/features/projects/builds_spec.rb44
-rw-r--r--spec/features/projects/commit/builds_spec.rb2
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb (renamed from spec/features/projects/commits/cherry_pick_spec.rb)0
-rw-r--r--spec/features/projects/compare_spec.rb (renamed from spec/features/compare_spec.rb)0
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/find_file_keyboard_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb3
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb3
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin682154 -> 681799 bytes
-rw-r--r--spec/features/projects/issuable_templates_spec.rb40
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb11
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb16
-rw-r--r--spec/features/projects/new_project_spec.rb20
-rw-r--r--spec/features/projects/pages_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb60
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/features/projects/ref_switcher_spec.rb5
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb31
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb21
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/view_on_env_spec.rb140
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb12
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb12
-rw-r--r--spec/features/security/project/internal_access_spec.rb14
-rw-r--r--spec/features/security/project/private_access_spec.rb14
-rw-r--r--spec/features/security/project/public_access_spec.rb14
-rw-r--r--spec/features/snippets/user_snippets_spec.rb49
-rw-r--r--spec/features/todos/todos_filtering_spec.rb57
-rw-r--r--spec/features/todos/todos_spec.rb2
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/variables_spec.rb2
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb13
-rw-r--r--spec/finders/environments_finder_spec.rb110
-rw-r--r--spec/finders/group_members_finder_spec.rb32
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/finders/notes_finder_spec.rb10
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml42
-rw-r--r--spec/fixtures/pages.tar.gzbin0 -> 1795 bytes
-rw-r--r--spec/fixtures/pages.zipbin0 -> 1851 bytes
-rw-r--r--spec/fixtures/pages.zip.metabin0 -> 225 bytes
-rw-r--r--spec/fixtures/pages_empty.tar.gzbin0 -> 128 bytes
-rw-r--r--spec/fixtures/pages_empty.zipbin0 -> 160 bytes
-rw-r--r--spec/fixtures/pages_empty.zip.metabin0 -> 116 bytes
-rw-r--r--spec/helpers/commits_helper_spec.rb19
-rw-r--r--spec/helpers/diff_helper_spec.rb10
-rw-r--r--spec/helpers/issuables_helper_spec.rb40
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb92
-rw-r--r--spec/helpers/projects_helper_spec.rb1
-rw-r--r--spec/helpers/search_helper_spec.rb5
-rw-r--r--spec/javascripts/.eslintrc5
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es64
-rw-r--r--spec/javascripts/activities_spec.js.es67
-rw-r--r--spec/javascripts/awards_handler_spec.js8
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js4
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js4
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js8
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es640
-rw-r--r--spec/javascripts/boards/issue_card_spec.js.es6191
-rw-r--r--spec/javascripts/boards/issue_spec.js.es623
-rw-r--r--spec/javascripts/boards/list_spec.js.es623
-rw-r--r--spec/javascripts/boards/mock_data.js.es65
-rw-r--r--spec/javascripts/boards/modal_store_spec.js.es6132
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js.es626
-rw-r--r--spec/javascripts/build_spec.js.es615
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js.es692
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js.es6105
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js.es633
-rw-r--r--spec/javascripts/commits_spec.js.es620
-rw-r--r--spec/javascripts/dashboard_spec.js.es66
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es62
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es67
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js.es63
-rw-r--r--spec/javascripts/environments/environment_external_url_spec.js.es63
-rw-r--r--spec/javascripts/environments/environment_item_spec.js.es67
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es64
-rw-r--r--spec/javascripts/environments/environment_spec.js.es614
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js.es64
-rw-r--r--spec/javascripts/environments/environments_store_spec.js.es65
-rw-r--r--spec/javascripts/environments/mock_data.js.es616
-rw-r--r--spec/javascripts/extensions/array_spec.js.es62
-rw-r--r--spec/javascripts/extensions/element_spec.js.es62
-rw-r--r--spec/javascripts/extensions/jquery_spec.js2
-rw-r--r--spec/javascripts/extensions/object_spec.js.es62
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js.es675
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js.es68
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es66
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js.es621
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es64
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es66
-rw-r--r--spec/javascripts/fixtures/branches.rb28
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml2
-rw-r--r--spec/javascripts/fixtures/header.html.haml35
-rw-r--r--spec/javascripts/fixtures/new_branch.html.haml4
-rw-r--r--spec/javascripts/fixtures/pipelines_table.html.haml2
-rw-r--r--spec/javascripts/fixtures/projects.rb28
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js.es66
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es615
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es63
-rw-r--r--spec/javascripts/gl_form_spec.js.es6123
-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.js8
-rw-r--r--spec/javascripts/helpers/class_spec_helper.js.es64
-rw-r--r--spec/javascripts/helpers/class_spec_helper_spec.js.es63
-rw-r--r--spec/javascripts/issuable_spec.js.es619
-rw-r--r--spec/javascripts/issuable_time_tracker_spec.js.es69
-rw-r--r--spec/javascripts/issue_spec.js4
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es620
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es682
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js.es616
-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.js84
-rw-r--r--spec/javascripts/merge_request_widget_spec.js30
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es68
-rw-r--r--spec/javascripts/new_branch_spec.js8
-rw-r--r--spec/javascripts/notes_spec.js8
-rw-r--r--spec/javascripts/pipelines_spec.js.es67
-rw-r--r--spec/javascripts/pretty_time_spec.js.es62
-rw-r--r--spec/javascripts/project_dashboard_spec.js.es686
-rw-r--r--spec/javascripts/project_title_spec.js31
-rw-r--r--spec/javascripts/right_sidebar_spec.js9
-rw-r--r--spec/javascripts/search_autocomplete_spec.js40
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js53
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js.es62
-rw-r--r--spec/javascripts/smart_interval_spec.js.es65
-rw-r--r--spec/javascripts/spec_helper.js48
-rw-r--r--spec/javascripts/subbable_resource_spec.js.es65
-rw-r--r--spec/javascripts/syntax_highlight_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js44
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js26
-rw-r--r--spec/javascripts/u2f/register_spec.js10
-rw-r--r--spec/javascripts/visibility_select_spec.js.es62
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js.es6 (renamed from spec/javascripts/vue_common_components/commit_spec.js.es6)2
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es687
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js.es664
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 (renamed from spec/javascripts/vue_pagination/pagination_spec.js.es6)18
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb2
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb32
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb35
-rw-r--r--spec/lib/event_filter_spec.rb18
-rw-r--r--spec/lib/gitlab/auth_spec.rb96
-rw-r--r--spec/lib/gitlab/chat_commands/command_spec.rb50
-rw-r--r--spec/lib/gitlab/chat_commands/deploy_spec.rb24
-rw-r--r--spec/lib/gitlab/chat_commands/issue_new_spec.rb (renamed from spec/lib/gitlab/chat_commands/issue_create_spec.rb)14
-rw-r--r--spec/lib/gitlab/chat_commands/issue_search_spec.rb12
-rw-r--r--spec/lib/gitlab/chat_commands/issue_show_spec.rb25
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/access_spec.rb49
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb47
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb17
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb23
-rw-r--r--spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/config/entry/coverage_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb2
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb45
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb76
-rw-r--r--spec/lib/gitlab/database_spec.rb16
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb8
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb12
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb6
-rw-r--r--spec/lib/gitlab/git_access_spec.rb10
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/highlight_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb46
-rw-r--r--spec/lib/gitlab/import_export/project.json13
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb70
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb15
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb4
-rw-r--r--spec/lib/gitlab/project_transfer_spec.rb (renamed from spec/lib/gitlab/uploads_transfer_spec.rb)11
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb75
-rw-r--r--spec/lib/gitlab/route_map_spec.rb90
-rw-r--r--spec/lib/gitlab/serializer/ci/variables_spec.rb (renamed from spec/lib/gitlab/serialize/ci/variables_spec.rb)2
-rw-r--r--spec/lib/gitlab/serializer/pagination_spec.rb49
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb18
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb18
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb6
-rw-r--r--spec/models/ability_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb45
-rw-r--r--spec/models/concerns/routable_spec.rb34
-rw-r--r--spec/models/cycle_analytics/code_spec.rb22
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb12
-rw-r--r--spec/models/cycle_analytics/production_spec.rb26
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb26
-rw-r--r--spec/models/cycle_analytics/test_spec.rb50
-rw-r--r--spec/models/deployment_spec.rb4
-rw-r--r--spec/models/environment_spec.rb84
-rw-r--r--spec/models/event_spec.rb24
-rw-r--r--spec/models/group_spec.rb13
-rw-r--r--spec/models/guest_spec.rb2
-rw-r--r--spec/models/issue_spec.rb65
-rw-r--r--spec/models/list_spec.rb39
-rw-r--r--spec/models/member_spec.rb8
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb22
-rw-r--r--spec/models/namespace_spec.rb56
-rw-r--r--spec/models/pages_domain_spec.rb168
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb2
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb16
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb5
-rw-r--r--spec/models/project_spec.rb275
-rw-r--r--spec/models/repository_spec.rb230
-rw-r--r--spec/models/route_spec.rb45
-rw-r--r--spec/models/timelog_spec.rb28
-rw-r--r--spec/models/todo_spec.rb8
-rw-r--r--spec/models/user_spec.rb57
-rw-r--r--spec/policies/ci/build_policy_spec.rb93
-rw-r--r--spec/policies/project_policy_spec.rb62
-rw-r--r--spec/policies/project_snippet_policy_spec.rb101
-rw-r--r--spec/requests/api/builds_spec.rb61
-rw-r--r--spec/requests/api/groups_spec.rb33
-rw-r--r--spec/requests/api/issues_spec.rb19
-rw-r--r--spec/requests/api/members_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb28
-rw-r--r--spec/requests/api/project_snippets_spec.rb62
-rw-r--r--spec/requests/api/projects_spec.rb109
-rw-r--r--spec/requests/api/snippets_spec.rb32
-rw-r--r--spec/requests/api/users_spec.rb19
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb172
-rw-r--r--spec/requests/api/v3/issues_spec.rb1259
-rw-r--r--spec/requests/api/v3/members_spec.rb342
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb726
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb188
-rw-r--r--spec/requests/api/v3/projects_spec.rb1424
-rw-r--r--spec/requests/ci/api/builds_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb12
-rw-r--r--spec/requests/lfs_http_spec.rb1
-rw-r--r--spec/routing/project_routing_spec.rb45
-rw-r--r--spec/rubocop/cop/gem_fetcher_spec.rb46
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_spec.rb41
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb33
-rw-r--r--spec/serializers/analytics_build_serializer_spec.rb10
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb5
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb5
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb14
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb19
-rw-r--r--spec/serializers/environment_serializer_spec.rb141
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb12
-rw-r--r--spec/services/boards/create_service_spec.rb7
-rw-r--r--spec/services/boards/issues/list_service_spec.rb5
-rw-r--r--spec/services/boards/issues/move_service_spec.rb49
-rw-r--r--spec/services/boards/lists/create_service_spec.rb4
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb9
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb9
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb4
-rw-r--r--spec/services/compare_service_spec.rb6
-rw-r--r--spec/services/event_create_service_spec.rb6
-rw-r--r--spec/services/files/update_service_spec.rb33
-rw-r--r--spec/services/git_hooks_service_spec.rb2
-rw-r--r--spec/services/groups/destroy_service_spec.rb (renamed from spec/services/destroy_group_service_spec.rb)16
-rw-r--r--spec/services/groups/update_service_spec.rb2
-rw-r--r--spec/services/issues/build_service_spec.rb11
-rw-r--r--spec/services/issues/create_service_spec.rb103
-rw-r--r--spec/services/labels/promote_service_spec.rb187
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb43
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb8
-rw-r--r--spec/services/notes/destroy_service_spec.rb (renamed from spec/services/notes/delete_service_spec.rb)2
-rw-r--r--spec/services/notification_service_spec.rb24
-rw-r--r--spec/services/pages_service_spec.rb47
-rw-r--r--spec/services/projects/create_service_spec.rb4
-rw-r--r--spec/services/projects/destroy_service_spec.rb49
-rw-r--r--spec/services/projects/transfer_service_spec.rb28
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb24
-rw-r--r--spec/services/projects/update_pages_service_spec.rb80
-rw-r--r--spec/services/search_service_spec.rb19
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb32
-rw-r--r--spec/services/spam_service_spec.rb47
-rw-r--r--spec/services/system_note_service_spec.rb53
-rw-r--r--spec/services/todo_service_spec.rb206
-rw-r--r--spec/services/users/destroy_spec.rb (renamed from spec/services/delete_user_service_spec.rb)11
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/api_helpers.rb9
-rw-r--r--spec/support/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb50
-rw-r--r--spec/support/drag_to_helper.rb13
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb35
-rw-r--r--spec/support/kubernetes_helpers.rb3
-rw-r--r--spec/support/matchers/match_file.rb5
-rw-r--r--spec/support/matchers/satisfy_matchers.rb19
-rw-r--r--spec/support/services/issuable_create_service_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/slash_commands_helpers.rb2
-rw-r--r--spec/support/test_env.rb3
-rw-r--r--spec/support/time_tracking_shared_examples.rb2
-rw-r--r--spec/tasks/config_lint_spec.rb27
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb14
-rw-r--r--spec/tasks/gitlab/mail_google_schema_whitelisting.rb2
-rw-r--r--spec/teaspoon_env.rb178
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb40
-rw-r--r--spec/workers/delete_user_worker_spec.rb4
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb3
-rw-r--r--spec/workers/post_receive_spec.rb6
-rw-r--r--spec/workers/project_destroy_worker_spec.rb4
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb8
-rw-r--r--vendor/assets/javascripts/date.format.js207
-rw-r--r--vendor/assets/javascripts/es6-promise.auto.js3
-rw-r--r--vendor/assets/javascripts/jquery.atwho.js1202
-rw-r--r--vendor/assets/javascripts/jquery.ba-resize.js246
-rw-r--r--vendor/assets/javascripts/jquery.caret.js436
-rw-r--r--vendor/assets/javascripts/jquery.turbolinks.js49
-rw-r--r--vendor/assets/javascripts/timeago.js237
-rw-r--r--vendor/assets/javascripts/u2f.js4
-rw-r--r--vendor/assets/javascripts/vue-resource.full.js1318
-rw-r--r--vendor/assets/javascripts/vue-resource.js.erb2
-rw-r--r--vendor/assets/javascripts/vue-resource.min.js7
-rw-r--r--vendor/assets/javascripts/vue.full.js7515
-rw-r--r--vendor/assets/javascripts/vue.js.erb2
-rw-r--r--vendor/assets/javascripts/vue.min.js7
-rw-r--r--vendor/assets/javascripts/xterm/fit.js4
-rw-r--r--vendor/gitignore/Android.gitignore8
-rw-r--r--vendor/gitignore/CMake.gitignore1
-rw-r--r--vendor/gitignore/CodeIgniter.gitignore6
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore24
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore3
-rw-r--r--vendor/gitignore/Global/Stata.gitignore24
-rw-r--r--vendor/gitignore/Go.gitignore30
-rw-r--r--vendor/gitignore/Java.gitignore6
-rw-r--r--vendor/gitignore/Joomla.gitignore27
-rw-r--r--vendor/gitignore/KiCad.gitignore3
-rw-r--r--vendor/gitignore/Laravel.gitignore4
-rw-r--r--vendor/gitignore/Magento.gitignore120
-rw-r--r--vendor/gitignore/Node.gitignore13
-rw-r--r--vendor/gitignore/Objective-C.gitignore3
-rw-r--r--vendor/gitignore/Perl.gitignore1
-rw-r--r--vendor/gitignore/PureScript.gitignore8
-rw-r--r--vendor/gitignore/Python.gitignore2
-rw-r--r--vendor/gitignore/Scala.gitignore2
-rw-r--r--vendor/gitignore/Swift.gitignore4
-rw-r--r--vendor/gitignore/Unity.gitignore4
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore4
-rw-r--r--vendor/gitignore/VisualStudio.gitignore8
-rw-r--r--vendor/gitignore/Waf.gitignore13
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml2
1524 files changed, 31265 insertions, 18848 deletions
diff --git a/.eslintignore b/.eslintignore
index b4bfa5a1f7a..c742b08c005 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,7 +1,9 @@
+/builds/
/coverage/
/coverage-javascript/
/node_modules/
/public/
/tmp/
/vendor/
-/builds/
+karma.config.js
+webpack.config.js
diff --git a/.eslintrc b/.eslintrc
index 9ab0145820d..1a2cd821af7 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -16,6 +16,8 @@
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
- "no-multiple-empty-lines": ["error", { "max": 1 }]
+ "no-multiple-empty-lines": ["error", { "max": 1 }],
+ "import/no-extraneous-dependencies": "off",
+ "import/no-unresolved": "off"
}
}
diff --git a/.flayignore b/.flayignore
index 44df2ba2371..fc64b0b5892 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1,3 +1,4 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
+app/policies/project_policy.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 782a43cbe74..733710bb005 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -107,11 +107,13 @@ setup-test-env:
<<: *dedicated-runner
stage: prepare
script:
- - bundle exec rake gitlab:assets:compile 2>/dev/null
+ - npm install
+ - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
expire_in: 7d
paths:
+ - node_modules
- public/assets
- tmp/tests
@@ -161,64 +163,7 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
-# Execute all testing suites against Ruby 2.1
-.ruby-21: &ruby-21
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
- <<: *use-db
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
- cache:
- key: "ruby21"
- paths:
- - vendor/ruby
-
-.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
- <<: *rspec-knapsack
- <<: *dedicated-runner
- <<: *ruby-21
-
-.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
- <<: *spinach-knapsack
- <<: *dedicated-runner
- <<: *ruby-21
-
-rspec 0 20 ruby21: *rspec-knapsack-ruby21
-rspec 1 20 ruby21: *rspec-knapsack-ruby21
-rspec 2 20 ruby21: *rspec-knapsack-ruby21
-rspec 3 20 ruby21: *rspec-knapsack-ruby21
-rspec 4 20 ruby21: *rspec-knapsack-ruby21
-rspec 5 20 ruby21: *rspec-knapsack-ruby21
-rspec 6 20 ruby21: *rspec-knapsack-ruby21
-rspec 7 20 ruby21: *rspec-knapsack-ruby21
-rspec 8 20 ruby21: *rspec-knapsack-ruby21
-rspec 9 20 ruby21: *rspec-knapsack-ruby21
-rspec 10 20 ruby21: *rspec-knapsack-ruby21
-rspec 11 20 ruby21: *rspec-knapsack-ruby21
-rspec 12 20 ruby21: *rspec-knapsack-ruby21
-rspec 13 20 ruby21: *rspec-knapsack-ruby21
-rspec 14 20 ruby21: *rspec-knapsack-ruby21
-rspec 15 20 ruby21: *rspec-knapsack-ruby21
-rspec 16 20 ruby21: *rspec-knapsack-ruby21
-rspec 17 20 ruby21: *rspec-knapsack-ruby21
-rspec 18 20 ruby21: *rspec-knapsack-ruby21
-rspec 19 20 ruby21: *rspec-knapsack-ruby21
-
-spinach 0 10 ruby21: *spinach-knapsack-ruby21
-spinach 1 10 ruby21: *spinach-knapsack-ruby21
-spinach 2 10 ruby21: *spinach-knapsack-ruby21
-spinach 3 10 ruby21: *spinach-knapsack-ruby21
-spinach 4 10 ruby21: *spinach-knapsack-ruby21
-spinach 5 10 ruby21: *spinach-knapsack-ruby21
-spinach 6 10 ruby21: *spinach-knapsack-ruby21
-spinach 7 10 ruby21: *spinach-knapsack-ruby21
-spinach 8 10 ruby21: *spinach-knapsack-ruby21
-spinach 9 10 ruby21: *spinach-knapsack-ruby21
-
# Other generic tests
-
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
@@ -232,7 +177,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
script:
- bundle exec $CI_BUILD_NAME
-rubocop:
+rubocop:
<<: *ruby-static-analysis
<<: *dedicated-runner
stage: test
@@ -241,6 +186,7 @@ rubocop:
rake haml_lint: *exec
rake scss_lint: *exec
+rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
@@ -271,7 +217,7 @@ rake db:migrate:reset:
<<: *use-db
<<: *dedicated-runner
script:
- - rake db:migrate:reset
+ - bundle exec rake db:migrate:reset
rake db:seed_fu:
stage: test
@@ -291,18 +237,17 @@ rake db:seed_fu:
paths:
- log/development.log
-teaspoon:
+karma:
cache:
paths:
- vendor/ruby
- - node_modules/
+ - node_modules
stage: test
<<: *use-db
<<: *dedicated-runner
script:
- - npm install
- npm link istanbul
- - rake teaspoon
+ - bundle exec rake karma
artifacts:
name: coverage-javascript
expire_in: 31d
@@ -353,10 +298,10 @@ migration paths:
- cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- - rake db:drop db:create db:schema:load db:seed_fu
+ - bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh
- - rake db:migrate
+ - bundle exec rake db:migrate
coverage:
stage: post-test
@@ -444,14 +389,14 @@ pages:
<<: *dedicated-runner
dependencies:
- coverage
- - teaspoon
+ - karma
- lint:javascript:report
script:
- mv public/ .public/
- mkdir public/
- - mv coverage public/coverage-ruby
- - mv coverage-javascript/default/ public/coverage-javascript/
- - mv eslint-report.html public/
+ - mv coverage/ public/coverage-ruby/ || true
+ - mv coverage-javascript/default/ public/coverage-javascript/ || true
+ - mv eslint-report.html public/ || true
artifacts:
paths:
- public
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 6d7d88c6791..34c2e097ba8 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -6,13 +6,13 @@
(How one can reproduce the issue - this is very important)
-### Expected behavior
+### What is the current *bug* behavior?
-(What you should see instead)
+(What actually happens)
-### Actual behavior
+### What is the expected *correct* behavior?
-(What actually happens)
+(What you should see instead)
### Relevant logs and/or screenshots
@@ -23,23 +23,23 @@ logs, and code as it's very hard to read otherwise.)
(If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com)
-#### Results of GitLab application Check
+#### Results of GitLab environment info
(For installations with omnibus-gitlab package run and paste the output of:
-`sudo gitlab-rake gitlab:check SANITIZE=true`)
+`sudo gitlab-rake gitlab:env:info`)
(For installations from source run and paste the output of:
-`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
-
-(we will only investigate if the tests are passing)
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
-#### Results of GitLab environment info
+#### Results of GitLab application Check
(For installations with omnibus-gitlab package run and paste the output of:
-`sudo gitlab-rake gitlab:env:info`)
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
(For installations from source run and paste the output of:
-`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
### Possible fixes
diff --git a/.rubocop.yml b/.rubocop.yml
index bf2b2d8afc2..d30846e6e0b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml
AllCops:
- TargetRubyVersion: 2.1
+ TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
@@ -17,6 +17,7 @@ AllCops:
# Exclude some GitLab files
Exclude:
- 'vendor/**/*'
+ - 'node_modules/**/*'
- 'db/*'
- 'db/fixtures/**/*'
- 'tmp/**/*'
@@ -30,8 +31,7 @@ AllCops:
- 'lib/gitlab/seeder.rb'
- 'generator_templates/**/*'
-
-##################### Style ##################################
+# Style #######################################################################
# Check indentation of private/protected visibility modifiers.
Style/AccessModifierIndentation:
@@ -470,7 +470,7 @@ Style/WhileUntilModifier:
Style/WordArray:
Enabled: false
-#################### Metrics ################################
+# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
# branches, and conditions.
@@ -515,8 +515,7 @@ Metrics/PerceivedComplexity:
Enabled: true
Max: 18
-
-#################### Lint ################################
+# Lint ########################################################################
# Checks for useless access modifiers.
Lint/UselessAccessModifier:
@@ -678,8 +677,7 @@ Lint/UselessSetterCall:
Lint/Void:
Enabled: true
-
-##################### Performance ############################
+# Performance #################################################################
# Use `casecmp` rather than `downcase ==`.
Performance/Casecmp:
@@ -717,8 +715,7 @@ Performance/StringReplacement:
Performance/TimesMap:
Enabled: true
-
-##################### Rails ##################################
+# Rails #######################################################################
# Enables Rails cops.
Rails:
@@ -766,7 +763,7 @@ Rails/ReadWriteAttribute:
Rails/ScopeArgs:
Enabled: true
-##################### RSpec ##################################
+# RSpec #######################################################################
# Check that instances are not being stubbed globally.
RSpec/AnyInstance:
@@ -827,3 +824,9 @@ RSpec/NotToNot:
# Prefer using verifying doubles over normal doubles.
RSpec/VerifiedDoubles:
Enabled: false
+
+# Custom ######################################################################
+
+# Disallow the `git` and `github` arguments in the Gemfile.
+GemFetcher:
+ Enabled: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9712b32232e..71d38e5453d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,32 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.16.4 (2017-02-02)
+
+- Support non-ASCII characters in GFM autocomplete. !8729
+- Fix search bar search param encoding. !8753
+- Fix project name label's for reference in project settings. !8795
+- Fix filtering with multiple words. !8830
+- Fixed services form cancel not redirecting back the integrations settings view. !8843
+- Fix filtering usernames with multiple words. !8851
+- Improve performance of slash commands. !8876
+- Remove old project members when retrying an export.
+- Fix permalink discussion note being collapsed.
+- Add project ID index to `project_authorizations` table to optimize queries.
+- Check public snippets for spam.
+- 19164 Add settings dropdown to mobile screens.
+
+## 8.16.3 (2017-01-27)
+
+- Add caching of droplab ajax requests. !8725
+- Fix access to the wiki code via HTTP when repository feature disabled. !8758
+- Revert 3f17f29a. !8785
+- Fix race conditions for AuthorizedProjectsWorker.
+- Fix autocomplete initial undefined state.
+- Fix Error 500 when repositories contain annotated tags pointing to blobs.
+- Fix /explore sorting.
+- Fixed label dropdown toggle text not correctly updating.
+
## 8.16.2 (2017-01-25)
- allow issue filter bar to be operated with mouse only. !8681
@@ -18,7 +44,7 @@ entry.
- Prevent users from deleting system deploy keys via the project deploy key API.
- Upgrade omniauth gem to 1.3.2.
-## 8.16.0 (2017-02-22)
+## 8.16.0 (2017-01-22)
- Add LDAP Rake task to rename a provider. !2181
- Validate label's title length. !5767 (Tomáš Kukrál)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d404f1b91df..72cd57ad7ff 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -15,6 +15,7 @@
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- [Technical debt](#technical-debt)
+ - [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
@@ -88,6 +89,27 @@ contributing to GitLab.
Please see the [UX Guide for GitLab].
+## Release retrospective and kickoff
+
+### Retrospective
+
+After each release (usually on the 22nd of each month), we have a retrospective
+call where we discuss what went well, what went wrong, and what we can improve
+for the next release. The [retrospective notes] are public and you are invited
+to comment them.
+If you're interested, you can even join the [retrospective call][retro-kickoff-call].
+
+### Kickoff
+
+Before working on the next release (usually on the 8th of each month), we have a
+kickoff call to explain what we expect to ship in the next release. The
+[kickoff notes] are public and you are invited to comment them.
+If you're interested, you can even join the [kickoff call][retro-kickoff-call].
+
+[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
+[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
+[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
+
## Issue tracker
To get support for your particular problem please use the
@@ -209,6 +231,21 @@ for a release by the appropriate person.
Make sure to mention the merge request that the `technical debt` issue is
associated with in the description of the issue.
+### Stewardship
+
+For issues related to the open source stewardship of GitLab,
+there is the ~"stewardship" label.
+
+This label is to be used for issues in which the stewardship of GitLab
+is a topic of discussion. For instance if GitLab Inc. is planning to remove
+features from GitLab CE to make exclusive in GitLab EE, related issues
+would be labelled with ~"stewardship".
+
+A recent example of this was the issue for
+[bringing the time tracking API to GitLab CE][time-tracking-issue].
+
+[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084
+
## Merge requests
We welcome merge requests with fixes and improvements to GitLab code, tests,
@@ -393,7 +430,7 @@ merge request:
1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md)
1. [JavaScript (ES6)](https://github.com/airbnb/javascript)
-1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5)
+1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
new file mode 100644
index 00000000000..0d91a54c7d4
--- /dev/null
+++ b/GITLAB_PAGES_VERSION
@@ -0,0 +1 @@
+0.3.0
diff --git a/Gemfile b/Gemfile
index 37a7666602f..79433b12823 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,7 +7,6 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
-gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -36,7 +35,7 @@ gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt'
+gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -48,6 +47,9 @@ gem 'rqrcode-rails3', '~> 0.1.7'
gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
+# GitLab Pages
+gem 'validates_hostname', '~> 1.0.6'
+
# Browser detection
gem 'browser', '~> 2.2'
@@ -109,7 +111,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
-gem 'asciidoctor-plantuml', '0.0.6'
+gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
@@ -219,10 +221,12 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
+gem 'webpack-rails', '~> 0.9.9'
+gem 'rack-proxy', '~> 0.6.0'
+
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
-gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
@@ -280,7 +284,7 @@ group :development, :test do
gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
- gem 'rspec_profiling'
+ gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -292,13 +296,9 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
- gem 'teaspoon', '~> 1.1.0'
- gem 'teaspoon-jasmine', '~> 2.2.0'
-
gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
- gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.46.0', require: false
gem 'rubocop-rspec', '~> 1.9.1', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 671d7788a86..235426afa49 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -54,7 +54,7 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
- asciidoctor-plantuml (0.0.6)
+ asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
ast (2.3.0)
attr_encrypted (3.0.3)
@@ -72,10 +72,6 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
- babel-source (5.8.35)
- babel-transpiler (0.7.0)
- babel-source (>= 4.0, < 6)
- execjs (~> 2.0)
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
@@ -266,8 +262,6 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.1)
- gitlab-turbolinks-classic (2.5.6)
- coffee-rails
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -379,7 +373,7 @@ GEM
json (1.8.3)
json-schema (2.6.2)
addressable (~> 2.3.8)
- jwt (1.5.4)
+ jwt (1.5.6)
kaminari (0.17.0)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
@@ -548,6 +542,8 @@ GEM
rack (>= 1.1)
rack-protection (1.5.3)
rack
+ rack-proxy (0.6.0)
+ rack
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.7.1)
@@ -642,7 +638,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rspec_profiling (0.0.4)
+ rspec_profiling (0.0.5)
activerecord
pg
rails
@@ -735,20 +731,14 @@ GEM
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
spring (>= 0.9.1)
- spring-commands-teaspoon (0.0.2)
- spring (>= 0.9.1)
sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.2)
- babel-source (>= 5.8.11)
- babel-transpiler
- sprockets (>= 3.0.0)
sprockets-rails (3.1.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sqlite3 (1.3.11)
+ sqlite3 (1.3.13)
stackprof (0.2.10)
state_machines (0.4.0)
state_machines-activemodel (0.4.0)
@@ -761,10 +751,6 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- teaspoon (1.1.5)
- railties (>= 3.2.5, < 6)
- teaspoon-jasmine (2.2.0)
- teaspoon (>= 1.0.0)
temple (0.7.7)
test_after_commit (1.1.0)
activerecord (>= 3.2)
@@ -799,6 +785,9 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ validates_hostname (1.0.6)
+ activerecord (>= 3.0)
+ activesupport (>= 3.0)
version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
@@ -816,6 +805,8 @@ GEM
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
+ webpack-rails (0.9.9)
+ rails (>= 3.2.0)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -841,7 +832,7 @@ DEPENDENCIES
allocations (~> 1.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
- asciidoctor-plantuml (= 0.0.6)
+ asciidoctor-plantuml (= 0.0.7)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -891,7 +882,6 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
- gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
@@ -912,7 +902,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0)
jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2)
- jwt
+ jwt (~> 1.5.6)
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
@@ -955,6 +945,7 @@ DEPENDENCIES
rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
+ rack-proxy (~> 0.6.0)
rails (= 4.2.7.1)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
@@ -971,7 +962,7 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rspec_profiling
+ rspec_profiling (~> 0.0.5)
rubocop (~> 0.46.0)
rubocop-rspec (~> 1.9.1)
ruby-fogbugz (~> 0.2.1)
@@ -996,14 +987,10 @@ DEPENDENCIES
spring (~> 1.7.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
- spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.7.0)
- sprockets-es6 (~> 0.9.2)
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
- teaspoon (~> 1.1.0)
- teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
@@ -1014,12 +1001,14 @@ DEPENDENCIES
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
+ validates_hostname (~> 1.0.6)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
web-console (~> 2.0)
webmock (~> 1.21.0)
+ webpack-rails (~> 0.9.9)
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.7
+ 1.14.3
diff --git a/LICENSE b/LICENSE
index 1dc1bdb7411..ad4f2872db5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2016 GitLab B.V.
+Copyright (c) 2011-2017 GitLab B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/PROCESS.md b/PROCESS.md
index cbeb781cd3c..fead93bd4cf 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -12,167 +12,132 @@ etc.).
## Common actions
-### Issue team
-
-- Looks for issues without [workflow labels](#how-we-handle-issues) and triages
- issue
-- Closes invalid issues with a comment (duplicates,
- [fixed in newer version](#issue-fixed-in-newer-version),
- [issue report for old version](#issue-report-for-old-version), not a problem
- in GitLab, etc.)
-- Asks for feedback from issue reporter
- ([invalid issue reports](#improperly-formatted-issue),
- [format code](#code-format), etc.)
-- Monitors all issues for feedback (but especially ones commented on since
- automatically watching them)
-- Closes issues with no feedback from the reporter for two weeks
-
-### Merge marshall & merge request coach
-
-- Responds to merge requests the issue team mentions them in and monitors for
- new merge requests
-- Provides feedback to the merge request submitter to improve the merge request
- (style, tests, etc.)
-- Mark merge requests `Ready for Merge` when they meet the
- [contribution acceptance criteria]
-- Mention developer(s) based on the
- [list of members and their specialities][team]
-- Closes merge requests with no feedback from the reporter for two weeks
-
-## Priorities of the issue team
-
-1. Mentioning people (critical)
-1. Workflow labels (normal)
-1. Functional labels (minor)
-1. Assigning issues (avoid if possible)
-
-## Mentioning people
+### Issue triaging
+
+Our issue triage policies are [described in our handbook]. You are very welcome
+to help the GitLab team triage issues. We also organize [issue bash events] once
+every quarter.
The most important thing is making sure valid issues receive feedback from the
development team. Therefore the priority is mentioning developers that can help
on those issues. Please select someone with relevant experience from
-[GitLab core team][core-team]. If there is nobody mentioned with that expertise
+[GitLab team][team]. If there is nobody mentioned with that expertise
look in the commit history for the affected files to find someone. Avoid
mentioning the lead developer, this is the person that is least likely to give a
timely response. If the involvement of the lead developer is needed the other
core team members will mention this person.
-## Workflow labels
+[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/
+[issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815
+
+### Merge request coaching
-Workflow labels are purposely not very detailed since that would be hard to keep
-updated as you would need to re-evaluate them after every comment. We optionally
-use functional labels on demand when we want to group related issues to get an
-overview (for example all issues related to RVM, to tackle them in one go) and
-to add details to the issue.
+Several people from the [GitLab team][team] are helping community members to get
+their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done).
-- ~"Awaiting Feedback" Feedback pending from the reporter
-- ~UX needs help from a UX designer
-- ~Frontend needs help from a Front-end engineer. Please follow the
- ["Implement design & UI elements" guidelines].
-- ~"Accepting Merge Requests" is a low priority, well-defined issue that we
- encourage people to contribute to. Not exclusive with other labels.
-- ~"feature proposal" is a proposal for a new feature for GitLab. People are encouraged to vote
-in support or comment for further detail. Do not use `feature request`.
-- ~bug is an issue reporting undesirable or incorrect behavior.
-- ~customer is an issue reported by enterprise subscribers. This label should
-be accompanied by *bug* or *feature proposal* labels.
+What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
-Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
+## Workflow labels
-## Functional labels
+Labelling issues is described in the [GitLab Inc engineering workflow].
-These labels describe what development specialities are involved such as: `CI`,
-`Core`, `Documentation`, `Frontend`, `Issues`, `Merge Requests`, `Omnibus`,
-`Release`, `Repository`, `UX`.
+[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
## Assigning issues
If an issue is complex and needs the attention of a specific person, assignment is a good option but assigning issues might discourage other people from contributing to that issue. We need all the contributions we can get so this should never be discouraged. Also, an assigned person might not have time for a few weeks, so others should feel free to takeover.
-## Label colors
-
-- Light orange `#fef2c0`: workflow labels for issue team members (awaiting
- feedback, awaiting confirmation of fix)
-- Bright orange `#eb6420`: workflow labels for core team members (attached MR,
- awaiting developer action/feedback)
-- Light blue `#82C5FF`: functional labels
-- Green labels `#009800`: issues that can generally be ignored. For example,
- issues given the following labels normally can be closed immediately:
- - Support (see copy & paste response:
- [Support requests and configuration questions](#support-requests-and-configuration-questions)
-
## Be kind
Be kind to people trying to contribute. Be aware that people may be a non-native
English speaker, they might not understand things or they might be very
sensitive as to how you word things. Use Emoji to express your feelings (heart,
-star, smile, etc.). Some good tips about giving feedback to merge requests is in
-the [Thoughtbot code review guide].
+star, smile, etc.). Some good tips about code reviews can be found in our
+[Code Review Guidelines].
+
+[Code Review Guidelines]: https://docs.gitlab.com/ce/development/code_review.html
## Feature Freeze
-5 working days before the 22nd the stable branches for the upcoming release will
-be frozen for major changes. Merge requests may still be merged into master
-during this period. By freezing the stable branches prior to a release there's
-no need to worry about last minute merge requests potentially breaking a lot of
-things.
+After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
+Merge requests may still be merged into master during this period,
+but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
+By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
+
+Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
+and security issues will be cherry-picked into the stable branch.
+Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
+These fixes will be released in the next RC (before the 22nd) or patch release (after the 22nd).
+
+If you think a merge request should go into the upcoming release even though it does not meet these requirements,
+you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
+
+1. a Release Manager
+2. an Engineering Lead
+3. an Engineering Director, the VP of Engineering, or the CTO
+
+You can find who is who on the [team page](https://about.gitlab.com/team/).
+
+Whether an exception is made is determined by weighing the benefit and urgency of the change
+(how important it is to the company that this is released _right now_ instead of in a month)
+against the potential negative impact
+(things breaking without enough time to comfortably find and fix them before the release on the 22nd).
+When in doubt, we err on the side of _not_ cherry-picking.
-What is considered to be a major change is determined on a case by case basis as
-this definition depends very much on the context of changes. For example, a 5
-line change might have a big impact on the entire application. Ultimately the
-decision will be made by those reviewing a merge request and the release
-manager.
+For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
+(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
-During the feature freeze all merge requests that are meant to go into the next
+During the feature freeze all merge requests that are meant to go into the upcoming
release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set. Merge requests without a milestone and this label will
+~"Pick into Stable" set, so that release managers can find and pick them.
+Merge requests without a milestone and this label will
not be merged into any stable branches.
## Copy & paste responses
### Improperly formatted issue
-Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Issue report for old version
-Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Support requests and configuration questions
Thanks for your interest in GitLab. We don't use the issue tracker for support
requests and configuration questions. Please check our
-\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available
-support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
+[getting help](https://about.gitlab.com/getting-help/) page to see all of the available
+support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md)
for more information.
### Code format
-Please use ``` to format console output, logs, and code as it's very hard to read otherwise.
+Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise.
### Issue fixed in newer version
-Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Improperly formatted merge request
-Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
+Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines).
### Inactivity close of an issue
-It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
+It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
### Inactivity close of a merge request
-This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
+This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request.
### Accepting merge requests
Is there an issue on the
-\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
+[issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is
similar to this? Could you please link it here?
Please be aware that new functionality that is not marked
-\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
+[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
might not make it into GitLab.
### Only accepting merge requests with green tests
@@ -187,9 +152,8 @@ rebase with master to see if that solves the issue.
We are currently in the process of closing down the issue tracker on GitHub, to
prevent duplication with the GitLab.com issue tracker.
Since this is an older issue I'll be closing this for now. If you think this is
-still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues).
+still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues).
-[core-team]: https://about.gitlab.com/core-team/
[team]: https://about.gitlab.com/team/
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 993f427c9fb..424dc719c78 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */
-/* global Turbolinks */
(function() {
this.Admin = (function() {
@@ -42,10 +41,10 @@
return $('.change-owner-link').show();
});
$('li.project_member').bind('ajax:success', function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.refreshCurrentPage();
});
$('li.group_member').bind('ajax:success', function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.refreshCurrentPage();
});
showBlacklistType = function() {
if ($("input[name='blacklist_type']:checked").val() === 'file') {
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 4849aab50f4..c9612784f9b 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */
/* global bp */
/* global Cookies */
/* global Flash */
@@ -6,65 +6,61 @@
/* global AwardsHandler */
/* global Aside */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require jquery2 */
-/*= require jquery-ui/autocomplete */
-/*= require jquery-ui/datepicker */
-/*= require jquery-ui/draggable */
-/*= require jquery-ui/effect-highlight */
-/*= require jquery-ui/sortable */
-/*= require jquery_ujs */
-/*= require jquery.endless-scroll */
-/*= require jquery.highlight */
-/*= require jquery.waitforimages */
-/*= require jquery.atwho */
-/*= require jquery.scrollTo */
-/*= require jquery.turbolinks */
-/*= require js.cookie */
-/*= require turbolinks */
-/*= require autosave */
-/*= require bootstrap/affix */
-/*= require bootstrap/alert */
-/*= require bootstrap/button */
-/*= require bootstrap/collapse */
-/*= require bootstrap/dropdown */
-/*= require bootstrap/modal */
-/*= require bootstrap/scrollspy */
-/*= require bootstrap/tab */
-/*= require bootstrap/transition */
-/*= require bootstrap/tooltip */
-/*= require bootstrap/popover */
-/*= require select2 */
-/*= require underscore */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
-/*= require shortcuts */
-/*= require shortcuts_navigation */
-/*= require shortcuts_dashboard_navigation */
-/*= require shortcuts_issuable */
-/*= require shortcuts_network */
-/*= require jquery.nicescroll */
-/*= require date.format */
-/*= require_directory ./behaviors */
-/*= require_directory ./blob */
-/*= require_directory ./templates */
-/*= require_directory ./commit */
-/*= require_directory ./extensions */
-/*= require_directory ./lib/utils */
-/*= require_directory ./u2f */
-/*= require_directory ./droplab */
-/*= require_directory . */
-/*= require fuzzaldrin-plus */
-/*= require es6-promise.auto */
+function requireAll(context) { return context.keys().map(context); }
+
+window.$ = window.jQuery = require('jquery');
+require('jquery-ui/ui/autocomplete');
+require('jquery-ui/ui/draggable');
+require('jquery-ui/ui/effect-highlight');
+require('jquery-ui/ui/sortable');
+require('jquery-ujs');
+require('vendor/jquery.endless-scroll');
+require('vendor/jquery.highlight');
+require('vendor/jquery.waitforimages');
+require('vendor/jquery.caret');
+require('vendor/jquery.atwho');
+require('vendor/jquery.scrollTo');
+window.Cookies = require('js-cookie');
+require('./autosave');
+require('bootstrap/js/affix');
+require('bootstrap/js/alert');
+require('bootstrap/js/button');
+require('bootstrap/js/collapse');
+require('bootstrap/js/dropdown');
+require('bootstrap/js/modal');
+require('bootstrap/js/scrollspy');
+require('bootstrap/js/tab');
+require('bootstrap/js/transition');
+require('bootstrap/js/tooltip');
+require('bootstrap/js/popover');
+require('select2/select2.js');
+window.Pikaday = require('pikaday');
+window._ = require('underscore');
+window.Dropzone = require('dropzone');
+window.Sortable = require('vendor/Sortable');
+require('mousetrap');
+require('mousetrap/plugins/pause/mousetrap-pause');
+require('./shortcuts');
+require('./shortcuts_navigation');
+require('./shortcuts_dashboard_navigation');
+require('./shortcuts_issuable');
+require('./shortcuts_network');
+require('vendor/jquery.nicescroll');
+requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
+require('vendor/fuzzaldrin-plus');
+window.ES6Promise = require('vendor/es6-promise.auto');
+window.ES6Promise.polyfill();
(function () {
- document.addEventListener('page:fetch', function () {
+ document.addEventListener('beforeunload', function () {
// Unbind scroll events
$(document).off('scroll');
// Close any open tooltips
@@ -252,5 +248,7 @@
new Aside();
// bind sidebar events
new gl.Sidebar();
+
+ gl.utils.initTimeagoTimeout();
});
}).call(this);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 629dc267337..9d776b74965 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,11 +1,13 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */
/* global Cookies */
+var emojiAliases = require('emoji-aliases');
+
(function() {
this.AwardsHandler = (function() {
var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence
function AwardsHandler() {
- this.aliases = gl.emojiAliases();
+ this.aliases = emojiAliases;
$(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
return function(e) {
e.stopPropagation();
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a6bc262b657..a489523b802 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,8 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */
/* global autosize */
-/*= require jquery.ba-resize */
-/*= require autosize */
+var autosize = require('vendor/autosize');
(function() {
$(function() {
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index d4895011be7..7747306688c 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -6,7 +6,7 @@
// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
// is submitted.
//
-/*= require extensions/jquery */
+require('../extensions/jquery');
//
// ### Example Markup
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index ccbd6b993cb..6276933e93e 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -4,7 +4,7 @@
// When called on a form with input fields with the `required` attribute, the
// form's submit button will be disabled until all required fields have values.
//
-/*= require extensions/jquery */
+require('../extensions/jquery');
//
// ### Example Markup
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 6a49715590c..a7181904ac9 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,6 +1,19 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */
(function(w) {
$(function() {
+ var toggleContainer = function(container, /* optional */toggleState) {
+ var $container = $(container);
+
+ $container
+ .find('.js-toggle-button .fa')
+ .toggleClass('fa-chevron-up', toggleState)
+ .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
+
+ $container
+ .find('.js-toggle-content')
+ .toggle(toggleState);
+ };
+
// Toggle button. Show/hide content inside parent container.
// Button does not change visibility. If button has icon - it changes chevron style.
//
@@ -10,14 +23,7 @@
//
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
- $(this)
- .find('.fa')
- .toggleClass('fa-chevron-down fa-chevron-up')
- .end()
- .closest('.js-toggle-container')
- .find('.js-toggle-content')
- .toggle()
- ;
+ toggleContainer($(this).closest('.js-toggle-container'));
});
// If we're accessing a permalink, ensure it is not inside a
@@ -26,8 +32,8 @@
var anchor = hash && document.getElementById(hash);
var container = anchor && $(anchor).closest('.js-toggle-container');
- if (container && container.find('.js-toggle-content').is(':hidden')) {
- container.find('.js-toggle-button').trigger('click');
+ if (container) {
+ toggleContainer(container, true);
anchor.scrollIntoView();
}
});
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index d3455fa3d8c..ec1c018424d 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,7 +1,8 @@
/* eslint-disable no-param-reassign, comma-dangle */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
+
((global) => {
class BlobCiYamlSelector extends gl.TemplateSelector {
requestFile(query) {
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
index bdf95017613..d4f60cc6ecd 100644
--- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
@@ -1,5 +1,6 @@
/* global Api */
-/*= require blob/template_selector */
+
+require('./template_selector');
(() => {
const global = window.gl || (window.gl = {});
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 5fd0857db29..1d0bcf6471f 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 7a14eb160d0..1d5672d4c48 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */
/* global Api */
-/*= require blob/template_selector */
+require('./template_selector');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index dfad9b2122b..9e0754819fa 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -2,7 +2,7 @@
/* global EditBlob */
/* global NewCommitForm */
-/*= require_tree . */
+require('./edit_blob');
(function() {
$(function() {
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index f9766471780..8f30900198e 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,23 +1,26 @@
-/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */
+/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */
/* global Vue */
/* global BoardService */
-//= require vue
-//= require vue-resource
-//= require Sortable
-//= require_tree ./models
-//= require_tree ./stores
-//= require_tree ./services
-//= require_tree ./mixins
-//= require_tree ./filters
-//= require ./components/board
-//= require ./components/board_sidebar
-//= require ./components/new_list_dropdown
-//= require ./vue_resource_interceptor
+function requireAll(context) { return context.keys().map(context); }
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/));
+require('./components/board');
+require('./components/board_sidebar');
+require('./components/new_list_dropdown');
+require('./components/modal/index');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
+ const ModalStore = gl.issueBoards.ModalStore;
window.gl = window.gl || {};
@@ -31,7 +34,8 @@ $(() => {
el: $boardApp,
components: {
'board': gl.issueBoards.Board,
- 'board-sidebar': gl.issueBoards.BoardSidebar
+ 'board-sidebar': gl.issueBoards.BoardSidebar,
+ 'board-add-issues-modal': gl.issueBoards.IssuesModal,
},
data: {
state: Store.state,
@@ -40,6 +44,8 @@ $(() => {
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
detailIssue: Store.detail
},
computed: {
@@ -48,7 +54,7 @@ $(() => {
},
},
created () {
- gl.boardService = new BoardService(this.endpoint, this.boardId);
+ gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
},
mounted () {
Store.disabled = this.disabled;
@@ -59,8 +65,6 @@ $(() => {
if (list.type === 'done') {
list.position = Infinity;
- } else if (list.type === 'backlog') {
- list.position = -1;
}
});
@@ -73,7 +77,7 @@ $(() => {
});
gl.IssueBoardsSearch = new Vue({
- el: '#js-boards-search',
+ el: document.getElementById('js-boards-search'),
data: {
filters: Store.state.filters
},
@@ -81,4 +85,27 @@ $(() => {
gl.issueBoards.newListDropdownInit();
}
});
+
+ gl.IssueBoardsModalAddBtn = new Vue({
+ mixins: [gl.issueBoards.ModalMixins],
+ el: document.getElementById('js-add-issues-btn'),
+ data: {
+ modal: ModalStore.store,
+ store: Store.state,
+ },
+ computed: {
+ disabled() {
+ return Store.shouldAddBlankState();
+ },
+ },
+ template: `
+ <button
+ class="btn btn-create pull-right prepend-left-10 has-tooltip"
+ type="button"
+ :disabled="disabled"
+ @click="toggleModal(true)">
+ Add issues
+ </button>
+ `,
+ });
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index a32881116d5..18324de18b3 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -2,9 +2,9 @@
/* global Vue */
/* global Sortable */
-//= require ./board_blank_state
-//= require ./board_delete
-//= require ./board_list
+require('./board_blank_state');
+require('./board_delete');
+require('./board_list');
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -22,7 +22,8 @@
props: {
list: Object,
disabled: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 5fc50280811..0ea66bd027c 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,6 +1,8 @@
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
/* global Vue */
+require('./issue_card_inner');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -9,12 +11,16 @@
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
- index: Number
+ index: Number,
+ rootPath: String,
},
data () {
return {
@@ -28,31 +34,6 @@
}
},
methods: {
- filterByLabel (label, e) {
- let labelToggleText = label.title;
- const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
- $(e.target).tooltip('hide');
-
- if (labelIndex === -1) {
- Store.state.filters['label_name'].push(label.title);
- $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
- } else {
- Store.state.filters['label_name'].splice(labelIndex, 1);
- labelToggleText = Store.state.filters['label_name'][0];
- $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
- }
-
- const selectedLabels = Store.state.filters['label_name'];
- if (selectedLabels.length === 0) {
- labelToggleText = 'Label';
- } else if (selectedLabels.length > 1) {
- labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
- }
-
- $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
-
- Store.updateFiltersUrl();
- },
mouseDown () {
this.showDetail = true;
},
@@ -71,6 +52,7 @@
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
+ Store.detail.list = this.list;
}
}
}
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 630fe084175..60b0a30af3f 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -2,8 +2,8 @@
/* global Vue */
/* global Sortable */
-//= require ./board_card
-//= require ./board_new_issue
+require('./board_card');
+require('./board_new_issue');
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -23,6 +23,7 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
+ rootPath: String,
},
data () {
return {
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 2386d3a613c..b5c14a198ba 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -37,6 +37,7 @@
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
+ Store.detail.list = this.list;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 02459722bbf..dfc6eed785c 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -5,6 +5,8 @@
/* global LabelsSelect */
/* global Sidebar */
+require('./sidebar/remove_issue');
+
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -18,7 +20,8 @@
data() {
return {
detail: Store.detail,
- issue: {}
+ issue: {},
+ list: {},
};
},
computed: {
@@ -29,7 +32,14 @@
watch: {
detail: {
handler () {
+ if (this.issue.id !== this.detail.issue.id) {
+ $('.js-issue-board-sidebar', this.$el).each((i, el) => {
+ $(el).data('glDropdown').clearMenu();
+ });
+ }
+
this.issue = this.detail.issue;
+ this.list = this.detail.list;
},
deep: true
},
@@ -54,6 +64,9 @@
new LabelsSelect();
new Sidebar();
gl.Subscription.bindAll('.subscription');
- }
+ },
+ components: {
+ removeBtn: gl.issueBoards.RemoveIssueBtn,
+ },
});
})();
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
new file mode 100644
index 00000000000..22a8b971ff8
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6
@@ -0,0 +1,111 @@
+/* global Vue */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.IssueCardInner = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: false,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ showLabel(label) {
+ if (!this.list) return true;
+
+ return !this.list.label || label.id !== this.list.label.id;
+ },
+ filterByLabel(label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters.label_name.indexOf(label.title);
+ $(e.currentTarget).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters.label_name.push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters.label_name.splice(labelIndex, 1);
+ labelToggleText = Store.state.filters.label_name[0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters.label_name;
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ },
+ labelStyle(label) {
+ return {
+ backgroundColor: label.color,
+ color: label.textColor,
+ };
+ },
+ },
+ template: `
+ <div>
+ <h4 class="card-title">
+ <i
+ class="fa fa-eye-slash confidential-icon"
+ v-if="issue.confidential"></i>
+ <a
+ :href="issueLinkBase + '/' + issue.id"
+ :title="issue.title">
+ {{ issue.title }}
+ </a>
+ </h4>
+ <div class="card-footer">
+ <span
+ class="card-number"
+ v-if="issue.id">
+ #{{ issue.id }}
+ </span>
+ <a
+ class="card-assignee has-tooltip"
+ :href="rootPath + issue.assignee.username"
+ :title="'Assigned to ' + issue.assignee.name"
+ v-if="issue.assignee"
+ data-container="body">
+ <img
+ class="avatar avatar-inline s20"
+ :src="issue.assignee.avatar"
+ width="20"
+ height="20"
+ :alt="'Avatar for ' + issue.assignee.name" />
+ </a>
+ <button
+ class="label color-label has-tooltip"
+ v-for="label in issue.labels"
+ type="button"
+ v-if="showLabel(label)"
+ @click="filterByLabel(label, $event)"
+ :style="labelStyle(label)"
+ :title="label.description"
+ data-container="body">
+ {{ label.title }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
new file mode 100644
index 00000000000..9538f5b69e9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6
@@ -0,0 +1,70 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalEmptyState = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ props: {
+ image: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ contents() {
+ const obj = {
+ title: 'You haven\'t added any issues to your project yet',
+ content: `
+ An issue can be a bug, a todo or a feature request that needs to be
+ discussed in a project. Besides, issues are searchable and filterable.
+ `,
+ };
+
+ if (this.activeTab === 'selected') {
+ obj.title = 'You haven\'t selected any issues yet';
+ obj.content = `
+ Go back to <strong>All issues</strong> and select some issues
+ to add to your board.
+ `;
+ }
+
+ return obj;
+ },
+ },
+ template: `
+ <section class="empty-state">
+ <div class="row">
+ <div class="col-xs-12 col-sm-6 col-sm-push-6">
+ <aside class="svg-content" v-html="image"></aside>
+ </div>
+ <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+ <div class="text-content">
+ <h4>{{ contents.title }}</h4>
+ <p v-html="contents.content"></p>
+ <a
+ :href="newIssuePath"
+ class="btn btn-success btn-inverted"
+ v-if="activeTab === 'all'">
+ New issue
+ </a>
+ <button
+ type="button"
+ class="btn btn-default"
+ @click="changeTab('all')"
+ v-if="activeTab === 'selected'">
+ All issues
+ </button>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6
new file mode 100644
index 00000000000..6de06811d94
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters.js.es6
@@ -0,0 +1,49 @@
+/* global Vue */
+const userFilter = require('./filters/user');
+const milestoneFilter = require('./filters/milestone');
+const labelFilter = require('./filters/label');
+
+module.exports = Vue.extend({
+ name: 'modal-filters',
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ destroyed() {
+ gl.issueBoards.ModalStore.setDefaultFilter();
+ },
+ components: {
+ userFilter,
+ milestoneFilter,
+ labelFilter,
+ },
+ template: `
+ <div class="modal-filters">
+ <user-filter
+ dropdown-class-name="dropdown-menu-author"
+ toggle-class-name="js-user-search js-author-search"
+ toggle-label="Author"
+ field-name="author_id"
+ :project-id="projectId"></user-filter>
+ <user-filter
+ dropdown-class-name="dropdown-menu-author"
+ toggle-class-name="js-assignee-search"
+ toggle-label="Assignee"
+ field-name="assignee_id"
+ :null-user="true"
+ :project-id="projectId"></user-filter>
+ <milestone-filter :milestone-path="milestonePath"></milestone-filter>
+ <label-filter :label-path="labelPath"></label-filter>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
new file mode 100644
index 00000000000..4fc8f72a145
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6
@@ -0,0 +1,54 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global LabelsSelect */
+module.exports = Vue.extend({
+ name: 'filter-label',
+ props: {
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ new LabelsSelect(this.$refs.dropdown);
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
+ type="button"
+ data-toggle="dropdown"
+ data-show-any="true"
+ data-show-no="true"
+ :data-labels="labelPath"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ Label
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
+ <div class="dropdown-title">
+ Filter by label
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search"
+ autocomplete="off" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
new file mode 100644
index 00000000000..d555599d300
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6
@@ -0,0 +1,55 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global MilestoneSelect */
+module.exports = Vue.extend({
+ name: 'filter-milestone',
+ props: {
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ new MilestoneSelect(null, this.$refs.dropdown);
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-milestone-select"
+ type="button"
+ data-toggle="dropdown"
+ data-show-any="true"
+ data-show-upcoming="true"
+ data-field-name="milestone_title"
+ :data-milestones="milestonePath"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ Milestone
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
+ <div class="dropdown-title">
+ <span>Filter by milestone</span>
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ placeholder="Search milestones"
+ autocomplete="off" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
new file mode 100644
index 00000000000..8523028c29c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6
@@ -0,0 +1,96 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global UsersSelect */
+module.exports = Vue.extend({
+ name: 'filter-user',
+ props: {
+ toggleClassName: {
+ type: String,
+ required: true,
+ },
+ dropdownClassName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ toggleLabel: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ nullUser: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ },
+ mounted() {
+ new UsersSelect(null, this.$refs.dropdown);
+ },
+ computed: {
+ currentUsername() {
+ return gon.current_username;
+ },
+ dropdownTitle() {
+ return `Filter by ${this.toggleLabel.toLowerCase()}`;
+ },
+ inputPlaceholder() {
+ return `Search ${this.toggleLabel.toLowerCase()}`;
+ },
+ },
+ template: `
+ <div class="dropdown">
+ <button
+ class="dropdown-menu-toggle js-user-search"
+ :class="toggleClassName"
+ type="button"
+ data-toggle="dropdown"
+ data-current-user="true"
+ :data-any-user="'Any ' + toggleLabel"
+ :data-null-user="nullUser"
+ :data-field-name="fieldName"
+ :data-project-id="projectId"
+ :data-first-user="currentUsername"
+ ref="dropdown">
+ <span class="dropdown-toggle-text">
+ {{ toggleLabel }}
+ </span>
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div
+ class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
+ :class="dropdownClassName">
+ <div class="dropdown-title">
+ {{ dropdownTitle }}
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ aria-label="Close"
+ type="button">
+ <i class="fa fa-times dropdown-menu-close-icon"></i>
+ </button>
+ </div>
+ <div class="dropdown-input">
+ <input
+ type="search"
+ class="dropdown-input-field"
+ autocomplete="off"
+ :placeholder="inputPlaceholder" />
+ <i class="fa fa-search dropdown-input-search"></i>
+ <i
+ role="button"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
+ </i>
+ </div>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
+ </div>
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6
new file mode 100644
index 00000000000..1cbc422c961
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/footer.js.es6
@@ -0,0 +1,83 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+
+require('./lists_dropdown');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooter = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return !ModalStore.selectedCount();
+ },
+ submitText() {
+ const count = ModalStore.selectedCount();
+
+ return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
+ },
+ },
+ methods: {
+ addIssues() {
+ const list = this.modal.selectedList || this.state.lists[0];
+ const selectedIssues = ModalStore.getSelectedIssues();
+ const issueIds = selectedIssues.map(issue => issue.globalId);
+
+ // Post the data to the backend
+ gl.boardService.bulkUpdate(issueIds, {
+ add_label_ids: [list.label.id],
+ }).catch(() => {
+ new Flash('Failed to update issues, please try again.', 'alert');
+
+ selectedIssues.forEach((issue) => {
+ list.removeIssue(issue);
+ list.issuesSize -= 1;
+ });
+ });
+
+ // Add the issues on the frontend
+ selectedIssues.forEach((issue) => {
+ list.addIssue(issue);
+ list.issuesSize += 1;
+ });
+
+ this.toggleModal(false);
+ },
+ },
+ components: {
+ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+ },
+ template: `
+ <footer
+ class="form-actions add-issues-footer">
+ <div class="pull-left">
+ <button
+ class="btn btn-success"
+ type="button"
+ :disabled="submitDisabled"
+ @click="addIssues">
+ {{ submitText }}
+ </button>
+ <span class="inline add-issues-footer-to-list">
+ to list
+ </span>
+ <lists-dropdown></lists-dropdown>
+ </div>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="toggleModal(false)">
+ Cancel
+ </button>
+ </footer>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6
new file mode 100644
index 00000000000..70c088f9054
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/header.js.es6
@@ -0,0 +1,90 @@
+/* global Vue */
+require('./tabs');
+const modalFilters = require('./filters');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalHeader = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectAllText() {
+ if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+ return 'Select all';
+ }
+
+ return 'Deselect all';
+ },
+ showSearch() {
+ return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
+ },
+ },
+ methods: {
+ toggleAll() {
+ this.$refs.selectAllBtn.blur();
+
+ ModalStore.toggleAll();
+ },
+ },
+ components: {
+ 'modal-tabs': gl.issueBoards.ModalTabs,
+ modalFilters,
+ },
+ template: `
+ <div>
+ <header class="add-issues-header form-actions">
+ <h2>
+ Add issues
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ aria-label="Close"
+ @click="toggleModal(false)">
+ <span aria-hidden="true">×</span>
+ </button>
+ </h2>
+ </header>
+ <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
+ <div
+ class="add-issues-search append-bottom-10"
+ v-if="showSearch">
+ <modal-filters
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-filters>
+ <input
+ placeholder="Search issues..."
+ class="form-control"
+ type="search"
+ v-model="searchTerm" />
+ <button
+ type="button"
+ class="btn btn-success btn-inverted prepend-left-10"
+ ref="selectAllBtn"
+ @click="toggleAll">
+ {{ selectAllText }}
+ </button>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6
new file mode 100644
index 00000000000..f290cd13763
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/index.js.es6
@@ -0,0 +1,163 @@
+/* global Vue */
+/* global ListIssue */
+
+require('./header');
+require('./list');
+require('./footer');
+require('./empty_state');
+
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.IssuesModal = Vue.extend({
+ props: {
+ blankStateImage: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ milestonePath: {
+ type: String,
+ required: true,
+ },
+ labelPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ page() {
+ this.loadIssues();
+ },
+ searchTerm() {
+ this.searchOperation();
+ },
+ showAddIssuesModal() {
+ if (this.showAddIssuesModal && !this.issues.length) {
+ this.loading = true;
+
+ this.loadIssues()
+ .then(() => {
+ this.loading = false;
+ });
+ } else if (!this.showAddIssuesModal) {
+ this.issues = [];
+ this.selectedIssues = [];
+ this.issuesCount = false;
+ }
+ },
+ filter: {
+ handler() {
+ this.loadIssues(true);
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ searchOperation: _.debounce(function searchOperationDebounce() {
+ this.loadIssues(true);
+ }, 500),
+ loadIssues(clearIssues = false) {
+ if (!this.showAddIssuesModal) return false;
+
+ const queryData = Object.assign({}, this.filter, {
+ search: this.searchTerm,
+ page: this.page,
+ per: this.perPage,
+ });
+
+ return gl.boardService.getBacklog(queryData).then((res) => {
+ const data = res.json();
+
+ if (clearIssues) {
+ this.issues = [];
+ }
+
+ data.issues.forEach((issueObj) => {
+ const issue = new ListIssue(issueObj);
+ const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+ issue.selected = !!foundSelectedIssue;
+
+ this.issues.push(issue);
+ });
+
+ this.loadingNewPage = false;
+
+ if (!this.issuesCount) {
+ this.issuesCount = data.size;
+ }
+ });
+ },
+ },
+ computed: {
+ showList() {
+ if (this.activeTab === 'selected') {
+ return this.selectedIssues.length > 0;
+ }
+
+ return this.issuesCount > 0;
+ },
+ showEmptyState() {
+ if (!this.loading && this.issuesCount === 0) {
+ return true;
+ }
+
+ return this.activeTab === 'selected' && this.selectedIssues.length === 0;
+ },
+ },
+ components: {
+ 'modal-header': gl.issueBoards.ModalHeader,
+ 'modal-list': gl.issueBoards.ModalList,
+ 'modal-footer': gl.issueBoards.ModalFooter,
+ 'empty-state': gl.issueBoards.ModalEmptyState,
+ },
+ template: `
+ <div
+ class="add-issues-modal"
+ v-if="showAddIssuesModal">
+ <div class="add-issues-container">
+ <modal-header
+ :project-id="projectId"
+ :milestone-path="milestonePath"
+ :label-path="labelPath">
+ </modal-header>
+ <modal-list
+ :image="blankStateImage"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ v-if="!loading && showList"></modal-list>
+ <empty-state
+ v-if="showEmptyState"
+ :image="blankStateImage"
+ :new-issue-path="newIssuePath"></empty-state>
+ <section
+ class="add-issues-list text-center"
+ v-if="loading">
+ <div class="add-issues-list-loading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+ </section>
+ <modal-footer></modal-footer>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6
new file mode 100644
index 00000000000..3730c1ecaeb
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/list.js.es6
@@ -0,0 +1,159 @@
+/* global Vue */
+/* global ListIssue */
+/* global bp */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalList = Vue.extend({
+ props: {
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ image: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return ModalStore.store;
+ },
+ watch: {
+ activeTab() {
+ if (this.activeTab === 'all') {
+ ModalStore.purgeUnselectedIssues();
+ }
+ },
+ },
+ computed: {
+ loopIssues() {
+ if (this.activeTab === 'all') {
+ return this.issues;
+ }
+
+ return this.selectedIssues;
+ },
+ groupedIssues() {
+ const groups = [];
+ this.loopIssues.forEach((issue, i) => {
+ const index = i % this.columns;
+
+ if (!groups[index]) {
+ groups.push([]);
+ }
+
+ groups[index].push(issue);
+ });
+
+ return groups;
+ },
+ },
+ methods: {
+ scrollHandler() {
+ const currentPage = Math.floor(this.issues.length / this.perPage);
+
+ if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
+ && currentPage === this.page) {
+ this.loadingNewPage = true;
+ this.page += 1;
+ }
+ },
+ toggleIssue(e, issue) {
+ if (e.target.tagName !== 'A') {
+ ModalStore.toggleIssue(issue);
+ }
+ },
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ showIssue(issue) {
+ if (this.activeTab === 'all') return true;
+
+ const index = ModalStore.selectedIssueIndex(issue);
+
+ return index !== -1;
+ },
+ setColumnCount() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'lg' || breakpoint === 'md') {
+ this.columns = 3;
+ } else if (breakpoint === 'sm') {
+ this.columns = 2;
+ } else {
+ this.columns = 1;
+ }
+ },
+ },
+ mounted() {
+ this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+ this.setColumnCountWrapper = this.setColumnCount.bind(this);
+ this.setColumnCount();
+
+ this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+ window.addEventListener('resize', this.setColumnCountWrapper);
+ },
+ beforeDestroy() {
+ this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+ window.removeEventListener('resize', this.setColumnCountWrapper);
+ },
+ components: {
+ 'issue-card-inner': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <section
+ class="add-issues-list add-issues-list-columns"
+ ref="list">
+ <div
+ class="empty-state add-issues-empty-state-filter text-center"
+ v-if="issuesCount > 0 && issues.length === 0">
+ <div
+ class="svg-content"
+ v-html="image">
+ </div>
+ <div class="text-content">
+ <h4>
+ There are no issues to show.
+ </h4>
+ </div>
+ </div>
+ <div
+ v-for="group in groupedIssues"
+ class="add-issues-list-column">
+ <div
+ v-for="issue in group"
+ v-if="showIssue(issue)"
+ class="card-parent">
+ <div
+ class="card"
+ :class="{ 'is-active': issue.selected }"
+ @click="toggleIssue($event, issue)">
+ <issue-card-inner
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath">
+ </issue-card-inner>
+ <span
+ :aria-label="'Issue #' + issue.id + ' selected'"
+ aria-checked="true"
+ v-if="issue.selected"
+ class="issue-card-selected text-center">
+ <i class="fa fa-check"></i>
+ </span>
+ </div>
+ </div>
+ </div>
+ </section>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
new file mode 100644
index 00000000000..3c05120a2da
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6
@@ -0,0 +1,56 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+ data() {
+ return {
+ modal: ModalStore.store,
+ state: gl.issueBoards.BoardsStore.state,
+ };
+ },
+ computed: {
+ selected() {
+ return this.modal.selectedList || this.state.lists[0];
+ },
+ },
+ destroyed() {
+ this.modal.selectedList = null;
+ },
+ template: `
+ <div class="dropdown inline">
+ <button
+ class="dropdown-menu-toggle"
+ type="button"
+ data-toggle="dropdown"
+ aria-expanded="false">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: selected.label.color }">
+ </span>
+ {{ selected.title }}
+ <i class="fa fa-chevron-down"></i>
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+ <ul>
+ <li
+ v-for="list in state.lists"
+ v-if="list.type == 'label'">
+ <a
+ href="#"
+ role="button"
+ :class="{ 'is-active': list.id == selected.id }"
+ @click.prevent="modal.selectedList = list">
+ <span
+ class="dropdown-label-box"
+ :style="{ backgroundColor: list.label.color }">
+ </span>
+ {{ list.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6
new file mode 100644
index 00000000000..e8cb43f3503
--- /dev/null
+++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6
@@ -0,0 +1,47 @@
+/* global Vue */
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalTabs = Vue.extend({
+ mixins: [gl.issueBoards.ModalMixins],
+ data() {
+ return ModalStore.store;
+ },
+ computed: {
+ selectedCount() {
+ return ModalStore.selectedCount();
+ },
+ },
+ destroyed() {
+ this.activeTab = 'all';
+ },
+ template: `
+ <div class="top-area prepend-top-10 append-bottom-10">
+ <ul class="nav-links issues-state-filters">
+ <li :class="{ 'active': activeTab == 'all' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('all')">
+ All issues
+ <span class="badge">
+ {{ issuesCount }}
+ </span>
+ </a>
+ </li>
+ <li :class="{ 'active': activeTab == 'selected' }">
+ <a
+ href="#"
+ role="button"
+ @click.prevent="changeTab('selected')">
+ Selected issues
+ <span class="badge">
+ {{ selectedCount }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
new file mode 100644
index 00000000000..e74935e1cb0
--- /dev/null
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6
@@ -0,0 +1,59 @@
+/* eslint-disable no-new */
+/* global Vue */
+/* global Flash */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.RemoveIssueBtn = Vue.extend({
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ list: {
+ type: Object,
+ required: true,
+ },
+ },
+ methods: {
+ removeIssue() {
+ const issue = this.issue;
+ const lists = issue.getLists();
+ const labelIds = lists.map(list => list.label.id);
+
+ // Post the remove data
+ gl.boardService.bulkUpdate([issue.globalId], {
+ remove_label_ids: labelIds,
+ }).catch(() => {
+ new Flash('Failed to remove issue from board, please try again.', 'alert');
+
+ lists.forEach((list) => {
+ list.addIssue(issue);
+ });
+ });
+
+ // Remove from the frontend store
+ lists.forEach((list) => {
+ list.removeIssue(issue);
+ });
+
+ Store.detail.issue = {};
+ },
+ },
+ template: `
+ <div
+ class="block list"
+ v-if="list.type !== 'done'">
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click="removeIssue">
+ Remove from board
+ </button>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 7e192e90fe6..ac2966cef5d 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,6 +1,7 @@
/* global Vue */
+/* global dateFormat */
Vue.filter('due-date', (value) => {
const date = new Date(value);
- return $.datepicker.formatDate('M d, yy', date);
+ return dateFormat(date, 'mmm d, yyyy');
});
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
new file mode 100644
index 00000000000..d378b7d4baf
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6
@@ -0,0 +1,14 @@
+(() => {
+ const ModalStore = gl.issueBoards.ModalStore;
+
+ gl.issueBoards.ModalMixins = {
+ methods: {
+ toggleModal(toggle) {
+ ModalStore.store.showAddIssuesModal = toggle;
+ },
+ changeTab(tab) {
+ ModalStore.store.activeTab = tab;
+ },
+ },
+ };
+})();
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 31531c3ee34..2d0a295ae4d 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -6,12 +6,15 @@
class ListIssue {
constructor (obj) {
+ this.globalId = obj.id;
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
this.subscribed = obj.subscribed;
this.labels = [];
+ this.selected = false;
+ this.assignee = false;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 3dd5f273057..5152be56b66 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -9,7 +9,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['done', 'blank'].indexOf(this.type) > -1;
this.filters = gl.issueBoards.BoardsStore.state.filters;
this.page = 1;
this.loading = true;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index ea55158306b..065e90518df 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -2,7 +2,13 @@
/* global Vue */
class BoardService {
- constructor (root, boardId) {
+ constructor (root, bulkUpdatePath, boardId) {
+ this.boards = Vue.resource(`${root}{/id}.json`, {}, {
+ issues: {
+ method: 'GET',
+ url: `${root}/${boardId}/issues.json`
+ }
+ });
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
@@ -10,7 +16,12 @@ class BoardService {
}
});
this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {});
+ this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
+ bulkUpdate: {
+ method: 'POST',
+ url: bulkUpdatePath,
+ },
+ });
Vue.http.interceptors.push((request, next) => {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
@@ -65,6 +76,20 @@ class BoardService {
issue
});
}
+
+ getBacklog(data) {
+ return this.boards.issues(data);
+ }
+
+ bulkUpdate(issueIds, extraData = {}) {
+ const data = {
+ update: Object.assign(extraData, {
+ issuable_ids: issueIds.join(','),
+ }),
+ };
+
+ return this.issues.bulkUpdate(data);
+ }
}
window.BoardService = BoardService;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index cdf1b09c0a4..50842ecbaaa 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -34,15 +34,10 @@
},
new (listObj) {
const list = this.addList(listObj);
- const backlogList = this.findList('type', 'backlog', 'backlog');
list
.save()
.then(() => {
- // 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();
@@ -52,7 +47,7 @@
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]);
+ return !(this.state.lists.filter(list => list.type !== 'done')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -102,7 +97,7 @@
listTo.addIssue(issue, listFrom, newIndex);
}
- if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ if (listTo.type === 'done') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6
new file mode 100644
index 00000000000..15fc6c79e8d
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/modal_store.js.es6
@@ -0,0 +1,107 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ class ModalStore {
+ constructor() {
+ this.store = {
+ columns: 3,
+ issues: [],
+ issuesCount: false,
+ selectedIssues: [],
+ showAddIssuesModal: false,
+ activeTab: 'all',
+ selectedList: null,
+ searchTerm: '',
+ loading: false,
+ loadingNewPage: false,
+ page: 1,
+ perPage: 50,
+ };
+
+ this.setDefaultFilter();
+ }
+
+ setDefaultFilter() {
+ this.store.filter = {
+ author_id: '',
+ assignee_id: '',
+ milestone_title: '',
+ label_name: [],
+ };
+ }
+
+ selectedCount() {
+ return this.getSelectedIssues().length;
+ }
+
+ toggleIssue(issueObj) {
+ const issue = issueObj;
+ const selected = issue.selected;
+
+ issue.selected = !selected;
+
+ if (!selected) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+
+ toggleAll() {
+ const select = this.selectedCount() !== this.store.issues.length;
+
+ this.store.issues.forEach((issue) => {
+ const issueUpdate = issue;
+
+ if (issueUpdate.selected !== select) {
+ issueUpdate.selected = select;
+
+ if (select) {
+ this.addSelectedIssue(issue);
+ } else {
+ this.removeSelectedIssue(issue);
+ }
+ }
+ });
+ }
+
+ getSelectedIssues() {
+ return this.store.selectedIssues.filter(issue => issue.selected);
+ }
+
+ addSelectedIssue(issue) {
+ const index = this.selectedIssueIndex(issue);
+
+ if (index === -1) {
+ this.store.selectedIssues.push(issue);
+ }
+ }
+
+ removeSelectedIssue(issue, forcePurge = false) {
+ if (this.store.activeTab === 'all' || forcePurge) {
+ this.store.selectedIssues = this.store.selectedIssues
+ .filter(fIssue => fIssue.id !== issue.id);
+ }
+ }
+
+ purgeUnselectedIssues() {
+ this.store.selectedIssues.forEach((issue) => {
+ if (!issue.selected) {
+ this.removeSelectedIssue(issue, true);
+ }
+ });
+ }
+
+ selectedIssueIndex(issue) {
+ return this.store.selectedIssues.indexOf(issue);
+ }
+
+ findSelectedIssue(issue) {
+ return this.store.selectedIssues
+ .filter(filteredIssue => filteredIssue.id === issue.id)[0];
+ }
+ }
+
+ gl.issueBoards.ModalStore = new ModalStore();
+})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
deleted file mode 100644
index 54c2b4ad369..00000000000
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */
-/* global Vue */
-
-Vue.http.interceptors.push((request, next) => {
- Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
-
- next(function (response) {
- Vue.activeResources -= 1;
- });
-});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index eae062a3aa3..f8dac1ff56e 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -43,6 +43,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
+ // TODO: Consider refactoring in light of turbolinks removal.
// the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0df84234520..c5a962dd199 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */
/* global Breakpoints */
-/* global Turbolinks */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -68,16 +67,8 @@
Build.prototype.initSidebar = function() {
this.$sidebar = $('.js-build-sidebar');
- this.sidebarTranslationLimits = {
- min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
- };
- this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight();
- this.$sidebar.css({
- top: this.sidebarTranslationLimits.max
- });
this.$sidebar.niceScroll();
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() {
@@ -127,7 +118,7 @@
pageUrl += DOWN_BUILD_TRACE;
}
- return Turbolinks.visit(pageUrl);
+ return gl.utils.visitUrl(pageUrl);
}
};
})(this)
@@ -232,14 +223,6 @@
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
- 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;
- this.$sidebar.css({
- top: newPosition
- });
- };
-
Build.prototype.toggleSidebar = function(shouldHide) {
var shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
this.$buildScroll.toggleClass('sidebar-expanded', shouldShow)
@@ -286,7 +269,7 @@
e.preventDefault();
$currentTarget = $(e.currentTarget);
$.scrollTo($currentTarget.attr('href'), {
- offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ offset: 0
});
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
new file mode 100644
index 00000000000..fbfec7743c7
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+require('./pipelines_table');
+/**
+ * Commits View > Pipelines Tab > Pipelines Table.
+ * Merge Request View > Pipelines Tab > Pipelines Table.
+ *
+ * Renders Pipelines table in pipelines tab in the commits show view.
+ * Renders Pipelines table in pipelines tab in the merge request show view.
+ */
+
+$(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ if (gl.commits.PipelinesTableBundle) {
+ gl.commits.PipelinesTableBundle.$destroy(true);
+ }
+
+ gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
new file mode 100644
index 00000000000..483b414126a
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6
@@ -0,0 +1,29 @@
+/* globals Vue */
+/* eslint-disable no-unused-vars, no-param-reassign */
+
+/**
+ * Pipelines service.
+ *
+ * Used to fetch the data used to render the pipelines table.
+ * Uses Vue.Resource
+ */
+class PipelinesService {
+ constructor(endpoint) {
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ /**
+ * Given the root param provided when the class is initialized, will
+ * make a GET request.
+ *
+ * @return {Promise}
+ */
+ all() {
+ return this.pipelines.get();
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
new file mode 100644
index 00000000000..f1b41911b73
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6
@@ -0,0 +1,50 @@
+/* eslint-disable no-underscore-dangle*/
+/**
+ * Pipelines' Store for commits view.
+ *
+ * Used to store the Pipelines rendered in the commit view in the pipelines table.
+ */
+
+class PipelinesStore {
+ constructor() {
+ this.state = {};
+ this.state.pipelines = [];
+ }
+
+ storePipelines(pipelines = []) {
+ this.state.pipelines = pipelines;
+
+ return pipelines;
+ }
+
+ /**
+ * Once the data is received we will start the time ago loops.
+ *
+ * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
+ * update the time to show how long as passed.
+ *
+ */
+ startTimeAgoLoops() {
+ const startTimeLoops = () => {
+ this.timeLoopInterval = setInterval(() => {
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
+ }, 10000);
+ };
+
+ startTimeLoops();
+
+ const removeIntervals = () => clearInterval(this.timeLoopInterval);
+ const startIntervals = () => startTimeLoops();
+
+ gl.VueRealtimeListener(removeIntervals, startIntervals);
+ }
+}
+
+window.gl = window.gl || {};
+gl.commits = gl.commits || {};
+gl.commits.pipelines = gl.commits.pipelines || {};
+gl.commits.pipelines.PipelinesStore = PipelinesStore;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
new file mode 100644
index 00000000000..ce0dbd4d56b
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6
@@ -0,0 +1,107 @@
+/* eslint-disable no-new, no-param-reassign */
+/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../lib/utils/common_utils');
+require('../../vue_shared/vue_resource_interceptor');
+require('../../vue_shared/components/pipelines_table');
+require('../../vue_realtime_listener/index');
+require('./pipelines_service');
+require('./pipelines_store');
+
+/**
+ *
+ * Uses `pipelines-table-component` to render Pipelines table with an API call.
+ * Endpoint is provided in HTML and passed as `endpoint`.
+ * We need a store to store the received environemnts.
+ * We need a service to communicate with the server.
+ *
+ * Necessary SVG in the table are provided as props. This should be refactored
+ * as soon as we have Webpack and can load them directly into JS files.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.commits = gl.commits || {};
+ gl.commits.pipelines = gl.commits.pipelines || {};
+
+ gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+
+ components: {
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+ },
+
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const svgsData = document.querySelector('.pipeline-svgs').dataset;
+ const store = new gl.commits.pipelines.PipelinesStore();
+
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
+
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ svgs: svgsObject,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
+
+ /**
+ * When the component is created the service to fetch the data will be
+ * initialized with the correct endpoint.
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ created() {
+ const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+
+ this.isLoading = true;
+ return pipelinesService.all()
+ .then(response => response.json())
+ .then((json) => {
+ this.store.storePipelines(json);
+ this.store.startTimeAgoLoops.call(this, Vue);
+ this.isLoading = false;
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+ });
+ },
+
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :svgs="svgs">
+ </pipelines-table-component>
+ </div>
+ </div>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6
index b94125a4210..4bd537a6f28 100644
--- a/app/assets/javascripts/copy_as_gfm.js.es6
+++ b/app/assets/javascripts/copy_as_gfm.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
/* jshint esversion: 6 */
-/*= require lib/utils/common_utils */
+require('./lib/utils/common_utils');
(() => {
const gfmRules = {
@@ -91,6 +91,9 @@
},
},
SanitizationFilter: {
+ 'a[name]:not([href]):empty'(el, text) {
+ return el.outerHTML;
+ },
'dl'(el, text) {
let lines = text.trim().split('\n');
// Add two spaces to the front of subsequent list items lines,
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 3485f8f91ed..0029c59e550 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
/* global Clipboard */
-/*= require clipboard */
+window.Clipboard = require('vendor/clipboard');
(function() {
var genericError, genericSuccess, showTooltip;
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6 b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
index 513298ba4e7..8652479e7bf 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js.es6
@@ -13,6 +13,12 @@
<div>
<div class="events-description">
{{ stage.description }}
+ <span v-if="items.length === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index 2f810a69758..dbdb01c8c68 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -2,9 +2,12 @@
/* global Cookies */
/* global Flash */
-//= require vue
-//= require_tree ./svg
-//= require_tree .
+window.Vue = require('vue');
+window.Cookies = require('js-cookie');
+
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/));
$(() => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6
index 35a029194d0..ccccd0a36ff 100644
--- a/app/assets/javascripts/diff.js.es6
+++ b/app/assets/javascripts/diff.js.es6
@@ -1,9 +1,10 @@
/* eslint-disable class-methods-use-this */
-//= require lib/utils/url_utility */
+require('./lib/utils/url_utility');
(() => {
const UNFOLD_COUNT = 20;
+ let isBound = false;
class Diff {
constructor() {
@@ -17,10 +18,12 @@
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
- $(document)
- .off('click', '.js-unfold, .diff-line-num a')
- .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
- .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ if (!isBound) {
+ $(document)
+ .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
+ .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
+ isBound = true;
+ }
this.openAnchoredDiff();
}
@@ -73,7 +76,7 @@
const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) {
- const clickTarget = $('.file-title, .click-to-expand', diffFile);
+ const clickTarget = $('.js-file-title, .click-to-expand', diffFile);
diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
this.highlighSelectedLine();
if (cb) cb();
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 2514459e65e..d948dff58ec 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,6 +1,6 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
-/* global Vue */
/* global CommentsStore */
+const Vue = require('vue');
(() => {
const CommentAndResolveBtn = Vue.extend({
@@ -9,13 +9,11 @@
},
data() {
return {
- textareaIsEmpty: true
+ textareaIsEmpty: true,
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return CommentsStore.state[this.discussionId];
- },
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
@@ -42,6 +40,9 @@
}
}
},
+ created() {
+ this.discussion = CommentsStore.state[this.discussionId];
+ },
mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index c3898873eaa..283dc330cad 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
-/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
+const Vue = require('vue');
(() => {
const JumpToDiscussion = Vue.extend({
@@ -12,12 +12,10 @@
data: function () {
return {
discussions: CommentsStore.state,
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
allResolved: function () {
return this.unresolvedDiscussionCount === 0;
},
@@ -183,10 +181,13 @@
}
$.scrollTo($target, {
- offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ offset: 0
});
}
- }
+ },
+ created() {
+ this.discussion = this.discussions[this.discussionId];
+ },
});
Vue.component('jump-to-discussion', JumpToDiscussion);
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 5852b8bbdb7..d1873d6c7a2 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,8 +1,8 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
-/* global Vue */
/* global CommentsStore */
/* global ResolveService */
/* global Flash */
+const Vue = require('vue');
(() => {
const ResolveBtn = Vue.extend({
@@ -10,14 +10,14 @@
noteId: Number,
discussionId: String,
resolved: Boolean,
- projectPath: String,
canResolve: Boolean,
resolvedBy: String
},
data: function () {
return {
discussions: CommentsStore.state,
- loading: false
+ loading: false,
+ note: {},
};
},
watch: {
@@ -30,13 +30,6 @@
discussion: function () {
return this.discussions[this.discussionId];
},
- note: function () {
- if (this.discussion) {
- return this.discussion.getNote(this.noteId);
- } else {
- return undefined;
- }
- },
buttonText: function () {
if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`;
@@ -73,10 +66,10 @@
if (this.isResolved) {
promise = ResolveService
- .unresolve(this.projectPath, this.noteId);
+ .unresolve(this.noteId);
} else {
promise = ResolveService
- .resolve(this.projectPath, this.noteId);
+ .resolve(this.noteId);
}
promise.then((response) => {
@@ -106,6 +99,8 @@
},
created: function () {
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+
+ this.note = this.discussion.getNote(this.noteId);
}
});
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 72cdae812bc..de9367f2136 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
-/* global Vue */
/* global DiscussionMixins */
/* global CommentsStore */
+const Vue = require('vue');
((w) => {
w.ResolveCount = Vue.extend({
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 ee5f62b2d9e..7c5fcd04d2d 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,25 +1,22 @@
/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
-/* global Vue */
/* global CommentsStore */
/* global ResolveService */
+const Vue = require('vue');
+
(() => {
const ResolveDiscussionBtn = Vue.extend({
props: {
discussionId: String,
mergeRequestId: Number,
- projectPath: String,
canResolve: Boolean,
},
data: function() {
return {
- discussions: CommentsStore.state
+ discussion: {},
};
},
computed: {
- discussion: function () {
- return this.discussions[this.discussionId];
- },
showButton: function () {
if (this.discussion) {
return this.discussion.isResolvable();
@@ -51,11 +48,13 @@
},
methods: {
resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
+ ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
}
},
created: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+
+ this.discussion = CommentsStore.state[this.discussionId];
}
});
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 1b3a57d0962..190461451d5 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,19 +1,24 @@
-/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */
/* global Vue */
/* global ResolveCount */
-//= require_directory ./models
-//= require_directory ./stores
-//= require_directory ./services
-//= require_directory ./mixins
-//= require_directory ./components
+function requireAll(context) { return context.keys().map(context); }
+const Vue = require('vue');
+requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
+requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
$(() => {
+ const projectPath = document.querySelector('.merge-request').dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
+ window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
+
gl.diffNotesCompileComponents = () => {
const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index a52c476352d..090c454e9e4 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,45 +1,37 @@
/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
-/* global Vue */
/* global Flash */
/* global CommentsStore */
-((w) => {
- class ResolveServiceClass {
- constructor() {
- this.noteResource = Vue.resource('notes{/noteId}/resolve');
- this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
- }
+const Vue = window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../../vue_shared/vue_resource_interceptor');
- setCSRF() {
- Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
- }
+(() => {
+ window.gl = window.gl || {};
- prepareRequest(root) {
- this.setCSRF();
- Vue.http.options.root = root;
+ class ResolveServiceClass {
+ constructor(root) {
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
}
- resolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
+ resolve(noteId) {
return this.noteResource.save({ noteId }, {});
}
- unresolve(projectPath, noteId) {
- this.prepareRequest(projectPath);
-
+ unresolve(noteId) {
return this.noteResource.delete({ noteId }, {});
}
- toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
+ toggleResolveForDiscussion(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
const isResolved = discussion.isResolved();
let promise;
if (isResolved) {
- promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
+ promise = this.unResolveAll(mergeRequestId, discussionId);
} else {
- promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
+ promise = this.resolveAll(mergeRequestId, discussionId);
}
promise.then((response) => {
@@ -62,11 +54,9 @@
});
}
- resolveAll(projectPath, mergeRequestId, discussionId) {
+ resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(projectPath);
-
discussion.loading = true;
return this.discussionResource.save({
@@ -75,11 +65,9 @@
}, {});
}
- unResolveAll(projectPath, mergeRequestId, discussionId) {
+ unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(projectPath);
-
discussion.loading = true;
return this.discussionResource.delete({
@@ -89,5 +77,5 @@
}
}
- w.ResolveService = new ResolveServiceClass();
-})(window);
+ gl.DiffNotesResolveServiceClass = ResolveServiceClass;
+})();
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 529d476ca4e..7eec2d39a9c 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -8,7 +8,6 @@
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
-/* global GLForm */
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
@@ -20,7 +19,6 @@
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
-/* global ShortcutsBlob */
/* global ProjectFork */
/* global BuildArtifacts */
/* global GroupsSelect */
@@ -37,6 +35,8 @@
/* global Labels */
/* global Shortcuts */
+const ShortcutsBlob = require('./shortcuts_blob');
+
(function() {
var Dispatcher;
@@ -64,17 +64,6 @@
new UsernameValidator();
new ActiveTabMemoizer();
break;
- case 'sessions:create':
- if (!gon.u2f) break;
- window.gl.u2fAuthenticate = new gl.U2FAuthenticate(
- $("#js-authenticate-u2f"),
- '#js-login-u2f-form',
- gon.u2f,
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- window.gl.u2fAuthenticate.start();
- break;
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
@@ -108,9 +97,10 @@
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
+ case 'projects:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
- new GLForm($('.milestone-form'));
+ new gl.GLForm($('.milestone-form'));
break;
case 'groups:milestones:new':
new ZenMode();
@@ -121,7 +111,7 @@
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
- new GLForm($('.issue-form'));
+ new gl.GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -131,7 +121,7 @@
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
- new GLForm($('.merge-request-form'));
+ new gl.GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
@@ -139,11 +129,11 @@
break;
case 'projects:tags:new':
new ZenMode();
- new GLForm($('.tag-form'));
+ new gl.GLForm($('.tag-form'));
break;
case 'projects:releases:edit':
new ZenMode();
- new GLForm($('.release-form'));
+ new gl.GLForm($('.release-form'));
break;
case 'projects:merge_requests:show':
new gl.Diff();
@@ -174,7 +164,7 @@
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
- });
+ }).bindEvents();
break;
case 'projects:commits:show':
case 'projects:activity':
@@ -237,7 +227,12 @@
case 'projects:blame:show':
new LineHighlighter();
shortcut_handler = new ShortcutsNavigation();
- new ShortcutsBlob(true);
+ const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
+ const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ });
break;
case 'groups:labels:new':
case 'groups:labels:edit':
@@ -271,7 +266,7 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
- case 'projects:variables:index':
+ case 'projects:ci_cd:show':
new gl.ProjectVariables();
break;
case 'ci:lints:create':
@@ -280,6 +275,17 @@
break;
}
switch (path.first()) {
+ case 'sessions':
+ case 'omniauth_callbacks':
+ if (!gon.u2f) break;
+ gl.u2fAuthenticate = new gl.U2FAuthenticate(
+ $('#js-authenticate-u2f'),
+ '#js-login-u2f-form',
+ gon.u2f,
+ document.querySelector('#js-login-2fa-device'),
+ document.querySelector('.js-2fa-form'),
+ );
+ gl.u2fAuthenticate.start();
case 'admin':
new Admin();
switch (path[1]) {
@@ -332,7 +338,7 @@
new gl.Wikis();
shortcut_handler = new ShortcutsNavigation();
new ZenMode();
- new GLForm($('.wiki-form'));
+ new gl.GLForm($('.wiki-form'));
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
@@ -357,7 +363,7 @@
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
- return new Shortcuts();
+ new Shortcuts();
}
};
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index c290e1a8355..5cdf11c6a2c 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -78,8 +78,8 @@ require('../window')(function(w){
},
destroy: function() {
- if (this.listTemplate) {
- var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
}
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 3d183f4ecb4..a510eebae1a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */
/* global Dropzone */
-/*= require preview_markdown */
+require('./preview_markdown');
(function() {
this.DropzoneInput = (function() {
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index d81d4cf8425..ab5ce23d261 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,6 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
+/* global dateFormat */
+/* global Pikaday */
(function(global) {
class DueDateSelect {
@@ -25,11 +27,14 @@
this.initGlDropdown();
this.initRemoveDueDate();
this.initDatePicker();
- this.initStopPropagation();
}
initGlDropdown() {
this.$dropdown.glDropdown({
+ opened: () => {
+ const calendar = this.$datePicker.data('pikaday');
+ calendar.show();
+ },
hidden: () => {
this.$selectbox.hide();
this.$value.css('display', '');
@@ -38,25 +43,37 @@
}
initDatePicker() {
- this.$datePicker.datepicker({
- dateFormat: 'yy-mm-dd',
- defaultDate: $("input[name='" + this.fieldName + "']").val(),
- altField: "input[name='" + this.fieldName + "']",
- onSelect: () => {
+ const $dueDateInput = $(`input[name='${this.fieldName}']`);
+
+ const calendar = new Pikaday({
+ field: $dueDateInput.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect: (dateText) => {
+ const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
+
+ $dueDateInput.val(formattedDate);
+
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $dueDateInput.val();
this.updateIssueBoardIssue();
} else {
- return this.saveDueDate(true);
+ this.saveDueDate(true);
}
}
});
+
+ this.$datePicker.append(calendar.el);
+ this.$datePicker.data('pikaday', calendar);
}
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
+ const calendar = this.$datePicker.data('pikaday');
e.preventDefault();
+ calendar.setDate(null);
+
if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
this.updateIssueBoardIssue();
@@ -67,12 +84,6 @@
});
}
- initStopPropagation() {
- $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', (e) => {
- return e.stopImmediatePropagation();
- });
- }
-
saveDueDate(isDropdown) {
this.parseSelectedDate();
this.prepSelectedDate();
@@ -86,7 +97,7 @@
// Construct Date object manually to avoid buggy dateString support within Date constructor
const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
- this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
+ this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
} else {
this.displayedDate = 'No due date';
}
@@ -153,14 +164,24 @@
}
initMilestoneDatePicker() {
- $('.datepicker').datepicker({
- dateFormat: 'yy-mm-dd'
+ $('.datepicker').each(function() {
+ const $datePicker = $(this);
+ const calendar = new Pikaday({
+ field: $datePicker.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect(dateText) {
+ $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
+ });
+
+ $datePicker.data('pikaday', calendar);
});
$('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault();
- const datepicker = $(e.target).siblings('.datepicker');
- $.datepicker._clearDate(datepicker);
+ const calendar = $(e.target).siblings('.datepicker').data('pikaday');
+ calendar.setDate(null);
});
}
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index fea642467fa..91553bda4dc 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -3,10 +3,10 @@
/* global EnvironmentsService */
/* global Flash */
-//= require vue
-//= require vue-resource
-//= require_tree ../services/
-//= require ./environment_item
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../services/environments_service');
+require('./environment_item');
(() => {
window.gl = window.gl || {};
@@ -180,9 +180,9 @@
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
- <th class="environments-build">Build</th>
+ <th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
- <th class="environments-date">Created</th>
+ <th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6
index 81468f4d3bc..ed1c78945db 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js.es6
+++ b/app/assets/javascripts/environments/components/environment_actions.js.es6
@@ -1,6 +1,7 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6
index 6592c1b5f0f..28cc0022d17 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js.es6
+++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6
@@ -1,6 +1,7 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 0e6bc3fdb2c..39746621c43 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -1,14 +1,15 @@
/* global Vue */
/* global timeago */
-/*= require timeago */
-/*= require lib/utils/text_utility */
-/*= require vue_common_component/commit */
-/*= require ./environment_actions */
-/*= require ./environment_external_url */
-/*= require ./environment_stop */
-/*= require ./environment_rollback */
-/*= require ./environment_terminal_button */
+window.Vue = require('vue');
+window.timeago = require('timeago.js');
+require('../../lib/utils/text_utility');
+require('../../vue_shared/components/commit');
+require('./environment_actions');
+require('./environment_external_url');
+require('./environment_stop');
+require('./environment_rollback');
+require('./environment_terminal_button');
(() => {
/**
@@ -146,12 +147,12 @@
},
/**
- * Returns the value of the `stoppable?` key provided in the response.
+ * Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
- isStoppable() {
- return this.model['stoppable?'];
+ hasStopAction() {
+ return this.model['stop_action?'];
},
/**
@@ -507,7 +508,7 @@
</external-url-component>
</div>
- <div v-if="isStoppable && canCreateDeployment"
+ <div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6
index b52298b4a88..5938340a128 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.js.es6
+++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6
@@ -1,6 +1,7 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6
index 0a29f2f36e9..be9526989a0 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js.es6
+++ b/app/assets/javascripts/environments/components/environment_stop.js.es6
@@ -1,6 +1,7 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
index 050184ba497..a3ad063f7cb 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6
@@ -1,6 +1,7 @@
-/*= require vue */
/* global Vue */
+window.Vue = require('vue');
+
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 3b003f6f661..05c59d92fd4 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -1,7 +1,7 @@
-//= require vue
-//= require_tree ./stores/
-//= require ./components/environment
-//= require ./vue_resource_interceptor
+window.Vue = require('vue');
+require('./stores/environments_store');
+require('./components/environment');
+require('../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6
index 575a45d9802..fab8d977f58 100644
--- a/app/assets/javascripts/environments/services/environments_service.js.es6
+++ b/app/assets/javascripts/environments/services/environments_service.js.es6
@@ -1,5 +1,6 @@
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
+
class EnvironmentsService {
constructor(root) {
diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
deleted file mode 100644
index 406bdbc1c7d..00000000000
--- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-/* 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.es6 b/app/assets/javascripts/extensions/array.js.es6
index cd401277689..f8256a8d26d 100644
--- a/app/assets/javascripts/extensions/array.js.es6
+++ b/app/assets/javascripts/extensions/array.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */
+/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, strict, max-len */
+
+'use strict';
+
Array.prototype.first = function() {
return this[0];
};
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
index 7d297b8eee8..572c221929a 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6
@@ -1,4 +1,4 @@
-/*= require filtered_search/filtered_search_dropdown */
+require('./filtered_search_dropdown');
/* global droplabFilter */
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
index 13cbec1be4a..b3dc3e502c5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6
@@ -1,4 +1,4 @@
-/*= require filtered_search/filtered_search_dropdown */
+require('./filtered_search_dropdown');
/* global droplabAjax */
/* global droplabFilter */
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
index 7bf199d9274..7e9c6f74aa5 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6
@@ -1,4 +1,4 @@
-/*= require filtered_search/filtered_search_dropdown */
+require('./filtered_search_dropdown');
/* global droplabAjaxFilter */
@@ -8,7 +8,7 @@
super(droplab, dropdown, input, filter);
this.config = {
droplabAjaxFilter: {
- endpoint: '/autocomplete/users.json',
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search',
params: {
per_page: 20,
@@ -39,8 +39,15 @@
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ let value = lastToken.value || '';
- return lastToken.value || '';
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if (value[0] === '"' || value[0] === '\'') {
+ value = value.slice(1);
+ }
+
+ return value;
}
init() {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index d188718c5f3..392f1835966 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,7 +1,3 @@
- // This is a manifest file that'll be compiled into including all the files listed below.
- // Add new JavaScript code in separate files in this directory and they'll automatically
- // be included in the compiled file accessible from http://example.com/assets/application.js
- // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
- // the compiled file.
- //
- /*= require_tree . */
+function requireAll(context) { return context.keys().map(context); }
+
+requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
index 859d6515531..e8c2df03a46 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6
@@ -4,7 +4,7 @@
class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
this.droplab = droplab;
- this.hookId = input.getAttribute('data-id');
+ this.hookId = input && input.getAttribute('data-id');
this.input = input;
this.filter = filter;
this.dropdown = dropdown;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
index 00e1c28692f..8ce4cf4fc36 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6
@@ -2,14 +2,15 @@
(() => {
class FilteredSearchDropdownManager {
- constructor() {
+ constructor(baseEndpoint = '') {
+ this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchInput = document.querySelector('.filtered-search');
this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('page:fetch', this.cleanupWrapper);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
}
cleanup() {
@@ -20,7 +21,7 @@
this.setupMapping();
- document.removeEventListener('page:fetch', this.cleanupWrapper);
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
}
setupMapping() {
@@ -38,13 +39,13 @@
milestone: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: ['milestones.json', '%'],
+ extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
element: document.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
- extraArguments: ['labels.json', '~'],
+ extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
element: document.querySelector('#js-dropdown-label'),
},
hint: {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index 029564ffc61..ffc7d29e4c5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -1,5 +1,3 @@
-/* global Turbolinks */
-
(() => {
class FilteredSearchManager {
constructor() {
@@ -8,20 +6,20 @@
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
- this.dropdownManager = new gl.FilteredSearchDropdownManager();
+ this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '');
this.bindEvents();
this.loadSearchParamsFromURL();
this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this);
- document.addEventListener('page:fetch', this.cleanupWrapper);
+ document.addEventListener('beforeunload', this.cleanupWrapper);
}
}
cleanup() {
this.unbindEvents();
- document.removeEventListener('page:fetch', this.cleanupWrapper);
+ document.removeEventListener('beforeunload', this.cleanupWrapper);
}
bindEvents() {
@@ -200,7 +198,9 @@
paths.push(`search=${sanitized}`);
}
- Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
+ const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`;
+
+ gl.utils.visitUrl(parameterizedUrl);
}
getUsernameParams() {
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 3f23095dad9..7f1f2a5d278 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -83,12 +83,12 @@
_a = decodeURI("%C3%80");
_y = decodeURI("%C3%BF");
- regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
+ regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi');
match = regexp.exec(subtext);
if (match) {
- return match[2] || match[1];
+ return (match[1] || match[1] === "") ? match[1] : match[2];
} else {
return null;
}
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index cc1c0877cdf..77fa662892a 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
-/* global Turbolinks */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
@@ -249,7 +248,7 @@
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
- if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') {
+ if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
};
@@ -438,7 +437,7 @@
}
};
- GitLabDropdown.prototype.opened = function() {
+ GitLabDropdown.prototype.opened = function(e) {
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
@@ -458,6 +457,10 @@
this.positionMenuAbove();
}
+ if (this.options.opened) {
+ this.options.opened.call(this, e);
+ }
+
return this.dropdown.trigger('shown.gl.dropdown');
};
@@ -512,12 +515,17 @@
// Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) {
+ return this.clearMenu().append(html);
+ };
+
+ GitLabDropdown.prototype.clearMenu = function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one .dropdown-content";
}
- return $(selector, this.dropdown).empty().append(html);
+
+ return $(selector, this.dropdown).empty();
};
GitLabDropdown.prototype.renderItem = function(data, group, index) {
@@ -718,7 +726,7 @@
if ($el.length) {
var href = $el.attr('href');
if (href && href !== '#') {
- Turbolinks.visit(href);
+ gl.utils.visitUrl(href);
} else {
$el.first().trigger('click');
}
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 16be930a2f4..e9add115429 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
-//= require gl_field_error
+require('./gl_field_error');
((global) => {
const customValidationFlag = 'gl-field-error-ignore';
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
deleted file mode 100644
index 08b2494f3df..00000000000
--- a/app/assets/javascripts/gl_form.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
-/* global GitLab */
-/* global DropzoneInput */
-/* global autosize */
-
-(function() {
- this.GLForm = (function() {
- function GLForm(form) {
- this.form = form;
- this.textarea = this.form.find('textarea.js-gfm-input');
- // Before we start, we should clean up any previous data for this form
- this.destroy();
- // Setup the form
- this.setupForm();
- this.form.data('gl-form', this);
- }
-
- GLForm.prototype.destroy = function() {
- // Clean form listeners
- this.clearEventListeners();
- return this.form.data('gl-form', null);
- };
-
- GLForm.prototype.setupForm = function() {
- var isNewForm;
- isNewForm = this.form.is(':not(.gfm-form)');
- this.form.removeClass('js-new-note-form');
- if (isNewForm) {
- this.form.find('.div-dropzone').remove();
- this.form.addClass('gfm-form');
- // remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
- gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
- new DropzoneInput(this.form);
- autosize(this.textarea);
- // form and textarea event listeners
- this.addEventListeners();
- }
- gl.text.init(this.form);
- // hide discard button
- this.form.find('.js-note-discard').hide();
- return this.form.show();
- };
-
- GLForm.prototype.clearEventListeners = function() {
- this.textarea.off('focus');
- this.textarea.off('blur');
- return gl.text.removeListeners(this.form);
- };
-
- GLForm.prototype.addEventListeners = function() {
- this.textarea.on('focus', function() {
- return $(this).closest('.md-area').addClass('is-focused');
- });
- return this.textarea.on('blur', function() {
- return $(this).closest('.md-area').removeClass('is-focused');
- });
- };
-
- return GLForm;
- })();
-}).call(this);
diff --git a/app/assets/javascripts/gl_form.js.es6 b/app/assets/javascripts/gl_form.js.es6
new file mode 100644
index 00000000000..0b446ff364a
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js.es6
@@ -0,0 +1,92 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
+(() => {
+ const global = window.gl || (window.gl = {});
+
+ function GLForm(form) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
+ this.destroy();
+ // Setup the form
+ this.setupForm();
+ this.form.data('gl-form', this);
+ }
+
+ GLForm.prototype.destroy = function() {
+ // Clean form listeners
+ this.clearEventListeners();
+ return this.form.data('gl-form', null);
+ };
+
+ GLForm.prototype.setupForm = function() {
+ var isNewForm;
+ isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ // remove notify commit author checkbox for non-commit notes
+ gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new DropzoneInput(this.form);
+ autosize(this.textarea);
+ // form and textarea event listeners
+ this.addEventListeners();
+ }
+ gl.text.init(this.form);
+ // hide discard button
+ this.form.find('.js-note-discard').hide();
+ this.form.show();
+ if (this.isAutosizeable) this.setupAutosize();
+ };
+
+ GLForm.prototype.setupAutosize = function () {
+ this.textarea.off('autosize:resized')
+ .on('autosize:resized', this.setHeightData.bind(this));
+
+ this.textarea.off('mouseup.autosize')
+ .on('mouseup.autosize', this.destroyAutosize.bind(this));
+
+ setTimeout(() => {
+ autosize(this.textarea);
+ this.textarea.css('resize', 'vertical');
+ }, 0);
+ };
+
+ GLForm.prototype.setHeightData = function () {
+ this.textarea.data('height', this.textarea.outerHeight());
+ };
+
+ GLForm.prototype.destroyAutosize = function () {
+ const outerHeight = this.textarea.outerHeight();
+
+ if (this.textarea.data('height') === outerHeight) return;
+
+ autosize.destroy(this.textarea);
+
+ this.textarea.data('height', outerHeight);
+ this.textarea.outerHeight(outerHeight);
+ this.textarea.css('max-height', window.outerHeight);
+ };
+
+ GLForm.prototype.clearEventListeners = function() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ return gl.text.removeListeners(this.form);
+ };
+
+ GLForm.prototype.addEventListeners = function() {
+ this.textarea.on('focus', function() {
+ return $(this).closest('.md-area').addClass('is-focused');
+ });
+ return this.textarea.on('blur', function() {
+ return $(this).closest('.md-area').removeClass('is-focused');
+ });
+ };
+
+ global.GLForm = GLForm;
+})();
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 32c26349da0..4f7777aa5bc 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,12 +1,3 @@
-/* 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
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 73715286c4a..d06a1a5dae4 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -5,7 +5,7 @@
/* global ContributorsStatGraphUtil */
/* global d3 */
-/*= require d3 */
+window.d3 = require('d3');
(function() {
this.ContributorsStatGraph = (function() {
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index cacfc177fc8..241249fae63 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -2,7 +2,7 @@
/* global d3 */
/* global ContributorsGraph */
-/*= require d3 */
+window.d3 = require('d3');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index a50bc4a9057..bc88dc2d092 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -59,11 +59,11 @@
} else {
avatar = gon.default_avatar_url;
}
- return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>";
+ return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
- return group.name;
+ return group.full_name;
};
return GroupsSelect;
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index f63d700fd65..8df86f68218 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,6 +1,5 @@
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
-/* global Turbolinks */
((global) => {
var issuable_created;
@@ -119,7 +118,7 @@
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
issuesUrl += formData;
- return Turbolinks.visit(issuesUrl);
+ return gl.utils.visitUrl(issuesUrl);
};
})(this),
initResetFilters: function() {
@@ -130,7 +129,7 @@
const baseIssuesUrl = target.href;
$form.attr('action', baseIssuesUrl);
- Turbolinks.visit(baseIssuesUrl);
+ gl.utils.visitUrl(baseIssuesUrl);
});
},
initChecks: function() {
diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6
index 7d0465aa8b4..e927cc0077c 100644
--- a/app/assets/javascripts/issuable/issuable_bundle.js.es6
+++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6
@@ -1 +1 @@
-//= require ./time_tracking/time_tracking_bundle
+require('./time_tracking/time_tracking_bundle');
diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
index 72433df2818..bf27fbac5d7 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
@@ -1,5 +1,5 @@
/* global Vue */
-//= require lib/utils/pretty_time
+require('../../../lib/utils/pretty_time');
(() => {
Vue.component('time-tracking-collapsed-state', {
@@ -39,4 +39,3 @@
`,
});
})();
-
diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
index 6abbd5dd167..750468c679b 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
@@ -1,5 +1,5 @@
/* global Vue */
-//= require lib/utils/pretty_time
+require('../../../lib/utils/pretty_time');
(() => {
const prettyTime = gl.utils.prettyTime;
diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
index 26563a7713b..e38f7852b1c 100644
--- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
@@ -1,10 +1,11 @@
/* global Vue */
-//= require ./help_state
-//= require ./collapsed_state
-//= require ./spent_only_pane
-//= require ./no_tracking_pane
-//= require ./estimate_only_pane
-//= require ./comparison_pane
+
+require('./help_state');
+require('./collapsed_state');
+require('./spent_only_pane');
+require('./no_tracking_pane');
+require('./estimate_only_pane');
+require('./comparison_pane');
(() => {
Vue.component('issuable-time-tracker', {
diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
index 0b8da2b1f4f..1ca01d3bdb9 100644
--- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
+++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
@@ -1,7 +1,8 @@
/* global Vue */
-//= require ./components/time_tracker
-//= require smart_interval
-//= require subbable_resource
+
+require('./components/time_tracker');
+require('../../smart_interval');
+require('../../subbable_resource');
(() => {
/* This Vue instance represents what will become the parent instance for the
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 293b856dc4d..2ec545db665 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -3,6 +3,8 @@
/* global UsersSelect */
/* global ZenMode */
/* global Autosave */
+/* global dateFormat */
+/* global Pikaday */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -13,7 +15,7 @@
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
- var $issuableDueDate;
+ var $issuableDueDate, calendar;
this.form = form;
this.toggleWip = bind(this.toggleWip, this);
this.renderWipExplanation = bind(this.renderWipExplanation, this);
@@ -35,12 +37,14 @@
this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
- $('.datepicker').datepicker({
- dateFormat: 'yy-mm-dd',
- onSelect: function(dateText, inst) {
- return $issuableDueDate.val(dateText);
+ calendar = new Pikaday({
+ field: $issuableDueDate.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ onSelect: function(dateText) {
+ $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
- }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()));
+ });
}
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 081b0d8b0d7..6c08b1b8e61 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
/* global Flash */
-/*= require flash */
-/*= require jquery.waitforimages */
-/*= require task_list */
+require('./flash');
+require('vendor/jquery.waitforimages');
+require('vendor/task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 8f48b1f57ce..38b2eb9ff14 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,5 +1,6 @@
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
/* global Flash */
+/* global Sortable */
((global) => {
class LabelManager {
@@ -8,11 +9,13 @@
this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
- this.prioritizedLabels.sortable({
- items: 'li',
- placeholder: 'list-placeholder',
- axis: 'y',
- update: this.onPrioritySortUpdate.bind(this)
+ this.emptyState = document.querySelector('#js-priority-labels-empty-state');
+ this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
+ filter: '.empty-message',
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ dataIdAttr: 'data-id',
+ onUpdate: this.onPrioritySortUpdate.bind(this),
});
this.bindEvents();
}
@@ -29,7 +32,12 @@
const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
$tooltip.tooltip('destroy');
- return _this.toggleLabelPriority($label, action);
+ _this.toggleLabelPriority($label, action);
+ _this.toggleEmptyState($label, $btn, action);
+ }
+
+ toggleEmptyState($label, $btn, action) {
+ this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li'));
}
toggleLabelPriority($label, action, persistState) {
@@ -45,13 +53,13 @@
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
- if ($from.find('li').length === 1) {
+ $label.detach().appendTo($target);
+ if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
- if (!$target.find('li').length) {
+ if ($target.find('> li:not(.empty-message)').length) {
$target.find('.empty-message').addClass('hidden');
}
- $label.detach().appendTo($target);
// Return if we are not persisting state
if (!persistState) {
return;
@@ -95,8 +103,12 @@
getSortedLabelsIds() {
const sortedIds = [];
- this.prioritizedLabels.find('li').each(function() {
- sortedIds.push($(this).data('id'));
+ this.prioritizedLabels.find('> li').each(function() {
+ const id = $(this).data('id');
+
+ if (id) {
+ sortedIds.push(id);
+ }
});
return sortedIds;
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 70dc0d06b7b..e4cf9057e6d 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,10 +4,17 @@
(function() {
this.LabelsSelect = (function() {
- function LabelsSelect() {
- var _this;
+ function LabelsSelect(els) {
+ var _this, $els;
_this = this;
- $('.js-label-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-label-select');
+ }
+
+ $els.each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
@@ -324,7 +331,7 @@
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e, isMarking) {
- var isIssueIndex, isMRIndex, page;
+ var isIssueIndex, isMRIndex, page, boardsModel;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
@@ -346,22 +353,31 @@
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
+ !$dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.BoardsStore.state.filters;
+ } else if ($dropdown.closest('.add-issues-modal').length) {
+ boardsModel = gl.issueBoards.ModalStore.store.filter;
+ }
+
+ if (boardsModel) {
if (label.isAny) {
- gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ boardsModel['label_name'] = [];
}
else if ($el.hasClass('is-active')) {
- gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ boardsModel['label_name'].push(label.title);
}
else {
- var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ var filters = boardsModel['label_name'];
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
});
- gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ boardsModel['label_name'] = filters;
}
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+ if (!$dropdown.closest('.add-issues-modal').length) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
e.preventDefault();
return;
}
diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
index 25e623f0fdc..976769ba84a 100644
--- a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
+++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb
@@ -7,19 +7,28 @@ ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort.
File.basename(file, '.js').sub(/^mode-/, '')
end
%>
-
+// Lazy-load configuration when ace.edit is called
(function() {
- window.gon = window.gon || {};
- var basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
- ace.config.set('basePath', basePath);
+ var basePath;
+ var ace = window.ace;
+ var edit = ace.edit;
+ ace.edit = function() {
+ window.gon = window.gon || {};
+ basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace';
+ ace.config.set('basePath', basePath);
- // configure paths for all worker modules
+ // configure paths for all worker modules
<% ace_workers.each do |worker| %>
- ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/worker-<%= worker %>.js');
+ ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= File.basename(asset_path("ace/worker-#{worker}.js")) %>');
<% end %>
- // configure paths for all mode modules
+ // configure paths for all mode modules
<% ace_modes.each do |mode| %>
- ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/mode-<%= mode %>.js');
+ ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= File.basename(asset_path("ace/mode-#{mode}.js")) %>');
<% end %>
+
+ // restore original method
+ ace.edit = edit;
+ return ace.edit.apply(ace, arguments);
+ };
})();
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index d8ad5aaeffe..9b011d89e93 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,7 +1,3 @@
/* eslint-disable func-names, space-before-function-paren */
-/*= require Chart */
-
-(function() {
-
-}).call(this);
+window.Chart = require('vendor/Chart');
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 57e7986576c..a9dd32edbed 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,7 +1,3 @@
/* eslint-disable func-names, space-before-function-paren */
-/*= require d3 */
-
-(function() {
-
-}).call(this);
+window.d3 = require('d3');
diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
index e810ee85bd3..2955bda1a36 100644
--- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
+++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6
@@ -95,7 +95,6 @@
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
history.replaceState({
- turbolinks: true,
url: newState,
}, document.title, newState);
return newState;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 51993bb3420..bcb3a706b51 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -69,27 +69,21 @@
var hash = w.gl.utils.getLocationHash();
if (!hash) return;
- var navbar = document.querySelector('.navbar-gitlab');
- var subnav = document.querySelector('.layout-nav');
- var fixedTabs = document.querySelector('.js-tabs-affix');
-
- var adjustment = 0;
- if (navbar) adjustment -= navbar.offsetHeight;
- if (subnav) adjustment -= subnav.offsetHeight;
+ // This is required to handle non-unicode characters in hash
+ hash = decodeURIComponent(hash);
// scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
- window.scrollBy(0, adjustment);
}
} else {
// only adjust for fixedTabs when not targeting user-generated content
+ var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
+ window.scrollBy(0, -fixedTabs.offsetHeight);
}
- window.scrollBy(0, adjustment);
}
};
@@ -134,14 +128,20 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
+ gl.utils.isMetaClick = function(e) {
+ // Identify following special clicks
+ // 1) Cmd + Click on Mac (e.metaKey)
+ // 2) Ctrl + Click on PC (e.ctrlKey)
+ // 3) Middle-click or Mouse Wheel Click (e.which is 2)
+ return e.metaKey || e.ctrlKey || e.which === 2;
+ };
+
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
- gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
- gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height();
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
return $('body, html').animate({
- scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight)
+ scrollTop: top - (gl.mrTabsHeight)
}, 200);
};
@@ -162,6 +162,7 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
+ if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents();
if (documentFragment.textContent.length === 0) return null;
@@ -229,5 +230,16 @@
return upperCaseHeaders;
};
+
+ /**
+ * Transforms a DOMStringMap into a plain object.
+ *
+ * @param {DOMStringMap} DOMStringMapObject
+ * @returns {Object}
+ */
+ w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => {
+ acc[element] = DOMStringMapObject[element];
+ return acc;
+ }, {});
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
deleted file mode 100644
index 3ed8bfd5651..00000000000
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
-/* global timeago */
-/* global dateFormat */
-
-/*= require timeago */
-/*= require date.format */
-
-(function() {
- (function(w) {
- var base;
- if (w.gl == null) {
- w.gl = {};
- }
- if ((base = w.gl).utils == null) {
- base.utils = {};
- }
- w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
-
- w.gl.utils.formatDate = function(datetime) {
- return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
- };
-
- w.gl.utils.getDayName = function(date) {
- return this.days[date.getDay()];
- };
-
- w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
- if (setTimeago == null) {
- setTimeago = true;
- }
-
- $timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
- 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>'
- });
- }
-
- $el.attr('data-timeago-rendered', true);
- gl.utils.renderTimeago($el);
- });
- };
-
- 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];
- };
-
- 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) {
- var millisecondsPerDay = 1000 * 60 * 60 * 24;
- var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
- var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
-
- return Math.floor((date2 - date1) / millisecondsPerDay);
- };
- })(window);
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.es6 b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
new file mode 100644
index 00000000000..f41fa15b147
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js.es6
@@ -0,0 +1,126 @@
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */
+/* global timeago */
+/* global dateFormat */
+
+window.timeago = require('timeago.js');
+window.dateFormat = require('vendor/date.format');
+
+(function() {
+ (function(w) {
+ var base;
+ var timeagoInstance;
+
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ w.gl.utils.formatDate = function(datetime) {
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ };
+
+ w.gl.utils.getDayName = function(date) {
+ return this.days[date.getDay()];
+ };
+
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
+ $timeagoEls.each((i, el) => {
+ el.setAttribute('title', gl.utils.formatDate(el.getAttribute('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>'
+ });
+ }
+
+ el.classList.add('js-timeago-render');
+ });
+
+ gl.utils.renderTimeago($timeagoEls);
+ };
+
+ w.gl.utils.getTimeago = function() {
+ var locale;
+
+ if (!timeagoInstance) {
+ 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];
+ };
+
+ timeago.register('gl_en', locale);
+ timeagoInstance = timeago();
+ }
+
+ return timeagoInstance;
+ };
+
+ 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.cachedTimeagoElements = [];
+ w.gl.utils.renderTimeago = function($els) {
+ if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
+ w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
+ } else if ($els) {
+ w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
+ }
+
+ w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
+ };
+
+ w.gl.utils.updateTimeagoText = function(el) {
+ const timeago = gl.utils.getTimeago();
+ const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
+
+ if (el.textContent !== formattedDate) {
+ el.textContent = formattedDate;
+ }
+ };
+
+ w.gl.utils.initTimeagoTimeout = function() {
+ gl.utils.renderTimeago();
+
+ gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
+ };
+
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ };
+ })(window);
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
deleted file mode 100644
index aeb86c9fa5b..00000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-(function() {
- gl.emojiAliases = function() {
- return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
- };
-
-}).call(this);
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 6bb575059b7..d9370db0cf2 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -161,6 +161,9 @@
gl.text.humanize = function(string) {
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
};
+ gl.text.pluralize = function(str, count) {
+ return str + (count > 1 || count === 0 ? 's' : '');
+ };
return gl.text.truncate = function(string, maxLength) {
return string.substr(0, (maxLength - 3)) + '...';
};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js.es6
index 8e15bf0735c..a1558b371f0 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js.es6
@@ -76,5 +76,11 @@
hashIndex = url.indexOf('#');
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
+
+ w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+
+ w.gl.utils.visitUrl = (url) => {
+ document.location.href = url;
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6
index eff1dcabfa2..49babdea2e1 100644
--- a/app/assets/javascripts/lib/vue_resource.js.es6
+++ b/app/assets/javascripts/lib/vue_resource.js.es6
@@ -1,2 +1,2 @@
-//= require vue
-//= require vue-resource
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 2f147704c22..d7137ec63e4 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -4,7 +4,7 @@
//
// Handles single- and multi-line selection and highlight for blob views.
//
-/*= require jquery.scrollTo */
+require('vendor/jquery.scrollTo');
//
// ### Example Markup
@@ -171,7 +171,6 @@
// This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
- turbolinks: false,
url: value
// We're using pushState instead of assigning location.hash directly to
// prevent the page from scrolling on the hashchange event
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index ea9bfb4860a..1b0d0768db8 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,14 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */
-/* global Turbolinks */
(function() {
- Turbolinks.enableProgressBar();
-
- $(document).on('page:fetch', function() {
+ window.addEventListener('beforeunload', function() {
$('.tanuki-logo').addClass('animate');
});
-
- $(document).on('page:change', function() {
- $('.tanuki-logo').removeClass('animate');
- });
}).call(this);
diff --git a/app/assets/javascripts/member_expiration_date.js.es6 b/app/assets/javascripts/member_expiration_date.js.es6
index bf6c0ec2798..f57d4a20498 100644
--- a/app/assets/javascripts/member_expiration_date.js.es6
+++ b/app/assets/javascripts/member_expiration_date.js.es6
@@ -1,3 +1,5 @@
+/* global Pikaday */
+/* global dateFormat */
(() => {
// 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
@@ -11,21 +13,34 @@
}
const inputs = $(selector);
- inputs.datepicker({
- dateFormat: 'yy-mm-dd',
- minDate: 1,
- onSelect: function onSelect() {
- $(this).trigger('change');
- toggleClearInput.call(this);
- },
+ inputs.each((i, el) => {
+ const $input = $(el);
+
+ const calendar = new Pikaday({
+ field: $input.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ minDate: new Date(),
+ onSelect(dateText) {
+ $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+
+ $input.trigger('change');
+
+ toggleClearInput.call($input);
+ },
+ });
+
+ $input.data('pikaday', calendar);
});
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
const input = $(this).closest('.clearable-input').find(selector);
- input.datepicker('setDate', null)
- .trigger('change');
+ const calendar = input.data('pikaday');
+
+ calendar.setDate(null);
+ input.trigger('change');
toggleClearInput.call(input);
});
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 a2d90f9ba47..653e52fb6bf 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -2,14 +2,14 @@
/* global Vue */
/* global Flash */
-//= require vue
-//= require ./merge_conflict_store
-//= require ./merge_conflict_service
-//= require ./mixins/line_conflict_utils
-//= require ./mixins/line_conflict_actions
-//= require ./components/diff_file_editor
-//= require ./components/inline_conflict_lines
-//= require ./components/parallel_conflict_lines
+window.Vue = require('vue');
+require('./merge_conflict_store');
+require('./merge_conflict_service');
+require('./mixins/line_conflict_utils');
+require('./mixins/line_conflict_actions');
+require('./components/diff_file_editor');
+require('./components/inline_conflict_lines');
+require('./components/parallel_conflict_lines');
$(() => {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 09ee8dbe9d7..e65378cd610 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,9 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */
/* global MergeRequestTabs */
-/*= require jquery.waitforimages */
-/*= require task_list */
-/*= require merge_request_tabs */
+require('vendor/jquery.waitforimages');
+require('vendor/task_list');
+require('./merge_request_tabs');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -110,22 +110,22 @@
};
MergeRequest.prototype.initCommitMessageListeners = function() {
- var textarea = $('textarea.js-commit-message');
-
- $('a.js-with-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-with-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithDescription'));
- $('p.js-with-description-hint').hide();
- $('p.js-without-description-hint').show();
+ $('.js-with-description-hint').hide();
+ $('.js-without-description-hint').show();
});
- $('a.js-without-description-link').on('click', function(e) {
+ $(document).on('click', 'a.js-without-description-link', function(e) {
+ var textarea = $('textarea.js-commit-message');
e.preventDefault();
textarea.val(textarea.data('messageWithoutDescription'));
- $('p.js-with-description-hint').show();
- $('p.js-without-description-hint').hide();
+ $('.js-with-description-hint').show();
+ $('.js-without-description-hint').hide();
});
};
diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6
index 4c8c28af755..cc049e00477 100644
--- a/app/assets/javascripts/merge_request_tabs.js.es6
+++ b/app/assets/javascripts/merge_request_tabs.js.es6
@@ -1,11 +1,11 @@
/* eslint-disable no-new, class-methods-use-this */
/* global Breakpoints */
/* global Cookies */
-/* global DiffNotesApp */
/* global Flash */
-/*= require js.cookie */
-/*= require breakpoints */
+require('./breakpoints');
+window.Cookies = require('js-cookie');
+require('./flash');
/* eslint-disable max-len */
// MergeRequestTabs
@@ -61,7 +61,6 @@
constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
- this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
@@ -83,12 +82,18 @@
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .on('click', this.clickTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
+
+ $('.merge-request-tabs a[data-toggle="tab"]')
+ .off('click', this.clickTab);
}
showTab(e) {
@@ -96,6 +101,14 @@
this.activateTab($(e.target).data('action'));
}
+ clickTab(e) {
+ if (e.target && gl.utils.isMetaClick(e)) {
+ const targetLink = e.target.getAttribute('href');
+ e.stopImmediatePropagation();
+ window.open(targetLink, '_blank');
+ }
+ }
+
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
@@ -112,14 +125,9 @@
if (this.diffViewType() === 'parallel') {
this.expandViewContainer();
}
- const navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo('.merge-request-details .merge-request-tabs', {
- offset: -navBarHeight,
+ offset: 0,
});
- } else if (action === 'pipelines') {
- this.loadPipelines($target.attr('href'));
- this.expandView();
- this.resetViewContainer();
} else {
this.expandView();
this.resetViewContainer();
@@ -131,11 +139,7 @@
scrollToElement(container) {
if (location.hash) {
- const offset = 0 - (
- $('.navbar-gitlab').outerHeight() +
- $('.layout-nav').outerHeight() +
- $('.js-tabs-affix').outerHeight()
- );
+ const offset = -$('.js-tabs-affix').outerHeight();
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
@@ -184,12 +188,13 @@
// Ensure parameters and hash come along for the ride
newState += location.search + location.hash;
+ // TODO: Consider refactoring in light of turbolinks removal.
+
// Replace the current history state with the new one without breaking
// Turbolinks' history.
//
// See https://github.com/rails/turbolinks/issues/363
window.history.replaceState({
- turbolinks: true,
url: newState,
}, document.title, newState);
@@ -243,25 +248,6 @@
});
}
- loadPipelines(source) {
- if (this.pipelinesLoaded) {
- return;
- }
- this.ajaxGet({
- url: `${source}.json`,
- success: (data) => {
- $('#pipelines').html(data.html);
- gl.utils.localTimeAgo($('.js-timeago', '#pipelines'));
- this.pipelinesLoaded = true;
- this.scrollToElement('#pipelines');
-
- new gl.MiniPipelineGraph({
- container: '.js-pipeline-table',
- });
- },
- });
- }
-
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
@@ -339,14 +325,12 @@
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
const $diffTabs = $('#diff-notes-app');
- const $fixedNav = $('.navbar-fixed-top');
- const $layoutNav = $('.layout-nav');
$tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({
offset: {
top: () => (
- $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - $layoutNav.height()
+ $diffTabs.offset().top - $tabs.height()
),
},
})
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 7cc319e2f4e..69aed77c83d 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -2,7 +2,8 @@
/* global notify */
/* global notifyPermissions */
/* global merge_request_widget */
-/* global Turbolinks */
+
+require('./smart_interval');
((global) => {
var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
@@ -50,6 +51,8 @@
this.getCIStatus(false);
this.retrieveSuccessIcon();
+ this.initMiniPipelineGraph();
+
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
@@ -65,17 +68,18 @@
incrementByFactorOf: 15000,
immediateExecution: true,
});
+
notifyPermissions();
}
MergeRequestWidget.prototype.clearEventListeners = function() {
- return $(document).off('page:change.merge_request');
+ return $(document).off('DOMContentLoaded');
};
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'pipelines', 'changes'];
- $(document).on('page:change.merge_request', (function(_this) {
+ $(document).on('DOMContentLoaded', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
@@ -150,16 +154,26 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
- if (data.status === '') {
+ if (!data.status) {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
- if (data.status !== _this.opts.ci_status && (data.status != null)) {
+ if (data.status !== _this.opts.ci_status ||
+ data.sha !== _this.opts.ci_sha ||
+ data.pipeline !== _this.opts.ci_pipeline) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
+ if (data.pipeline) {
+ _this.opts.ci_pipeline = data.pipeline;
+ _this.updatePipelineUrls(data.pipeline);
+ }
+ if (data.sha) {
+ _this.opts.ci_sha = data.sha;
+ _this.updateCommitUrls(data.sha);
+ }
if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
@@ -225,17 +239,20 @@
case "failed":
case "canceled":
case "not_found":
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
+ break;
case "running":
- return this.setMergeButtonClass('btn-info');
+ this.setMergeButtonClass('btn-info');
+ break;
case "success":
case "success_with_warnings":
- return this.setMergeButtonClass('btn-create');
+ this.setMergeButtonClass('btn-create');
}
} else {
$('.ci_widget.ci-error').show();
- return this.setMergeButtonClass('btn-danger');
+ this.setMergeButtonClass('btn-danger');
}
+ this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
@@ -248,6 +265,22 @@
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
+ MergeRequestWidget.prototype.updatePipelineUrls = function(id) {
+ const pipelineUrl = this.opts.pipeline_path;
+ $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/'));
+ };
+
+ MergeRequestWidget.prototype.updateCommitUrls = function(id) {
+ const commitsUrl = this.opts.commits_path;
+ $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
+ };
+
+ MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
+ new gl.MiniPipelineGraph({
+ container: '.js-pipeline-inline-mr-widget-graph:visible',
+ }).bindEvents();
+ };
+
return MergeRequestWidget;
})();
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
index 5969d2ba56b..5840916846b 100644
--- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
+++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6
@@ -47,7 +47,7 @@
$('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
});
} else {
- merge_request_widget.getMergeStatus();
+ setTimeout(() => merge_request_widget.getMergeStatus(), 200);
}
});
})();
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 7ce1259e015..051cb9fe5c5 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */
/* global Flash */
+/* global Sortable */
(function() {
this.Milestone = (function() {
@@ -8,11 +9,9 @@
type: "PUT",
url: issue_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -27,11 +26,9 @@
type: "PUT",
url: sort_issues_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function() {
return new Flash("Issues update failed", 'alert');
},
@@ -46,11 +43,9 @@
type: "PUT",
url: sort_mr_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -63,11 +58,9 @@
type: "PUT",
url: merge_request_url,
data: data,
- success: (function(_this) {
- return function(_data) {
- return _this.successCallback(_data, li);
- };
- })(this),
+ success: function(_data) {
+ return Milestone.successCallback(_data, li);
+ },
error: function(data) {
return new Flash("Issue update failed", 'alert');
},
@@ -81,65 +74,30 @@
img_tag = $('<img/>');
img_tag.attr('src', data.assignee.avatar_url);
img_tag.addClass('avatar s16');
- $(element).find('.assignee-icon').html(img_tag);
+ $(element).find('.assignee-icon img').replaceWith(img_tag);
} else {
- $(element).find('.assignee-icon').html('');
+ $(element).find('.assignee-icon').empty();
}
return $(element).effect('highlight');
};
function Milestone() {
var oldMouseStart;
- oldMouseStart = $.ui.sortable.prototype._mouseStart;
- $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
- this._trigger("beforeStart", event, this._uiHash());
- return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
- };
this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching();
}
Milestone.prototype.bindIssuesSorting = function() {
- return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
- connectWith: ".issues-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
- },
- stop: function(event, ui) {
- return $(".issues-sortable-list").css("min-height", "0px");
- },
- update: function(event, ui) {
- var data;
- // Prevents sorting from container which element has been removed.
- if ($(this).find(ui.item).length > 0) {
- data = $(this).sortable("serialize");
- return Milestone.sortIssues(data);
- }
- },
- receive: function(event, ui) {
- var data, issue_id, issue_url, new_state;
- new_state = $(this).data('state');
- issue_id = ui.item.data('iid');
- issue_url = ui.item.data('url');
- data = (function() {
- switch (new_state) {
- case 'ongoing':
- return "issue[assignee_id]=" + gon.current_user_id;
- case 'unassigned':
- return "issue[assignee_id]=";
- case 'closed':
- return "issue[state_event]=close";
- }
- })();
- if ($(ui.sender).data('state') === "closed") {
- data += "&issue[state_event]=reopen";
- }
- return Milestone.updateIssue(ui.item, issue_url, data);
- }
- }).disableSelection();
+ $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
+ this.createSortable(el, {
+ group: 'issue-list',
+ listEls: $('.issues-sortable-list'),
+ fieldName: 'issue',
+ sortCallback: Milestone.sortIssues,
+ updateCallback: Milestone.updateIssue,
+ });
+ }.bind(this));
};
Milestone.prototype.bindTabsSwitching = function() {
@@ -154,42 +112,62 @@
};
Milestone.prototype.bindMergeRequestSorting = function() {
- return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({
- connectWith: ".merge_requests-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
+ $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
+ this.createSortable(el, {
+ group: 'merge-request-list',
+ listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
+ fieldName: 'merge_request',
+ sortCallback: Milestone.sortMergeRequests,
+ updateCallback: Milestone.updateMergeRequest,
+ });
+ }.bind(this));
+ };
+
+ Milestone.prototype.createSortable = function(el, opts) {
+ return Sortable.create(el, {
+ group: opts.group,
+ filter: '.is-disabled',
+ forceFallback: true,
+ onStart: function(e) {
+ opts.listEls.css('min-height', e.item.offsetHeight);
},
- stop: function(event, ui) {
- return $(".merge_requests-sortable-list").css("min-height", "0px");
+ onEnd: function () {
+ opts.listEls.css("min-height", "0px");
},
- update: function(event, ui) {
- var data;
- data = $(this).sortable("serialize");
- return Milestone.sortMergeRequests(data);
+ onUpdate: function(e) {
+ var ids = this.toArray(),
+ data;
+
+ if (ids.length) {
+ data = ids.map(function(id) {
+ return 'sortable_' + opts.fieldName + '[]=' + id;
+ }).join('&');
+
+ opts.sortCallback(data);
+ }
},
- receive: function(event, ui) {
- var data, merge_request_id, merge_request_url, new_state;
- new_state = $(this).data('state');
- merge_request_id = ui.item.data('iid');
- merge_request_url = ui.item.data('url');
+ onAdd: function (e) {
+ var data, issuableId, issuableUrl, newState;
+ newState = e.to.dataset.state;
+ issuableUrl = e.item.dataset.url;
data = (function() {
- switch (new_state) {
+ switch (newState) {
case 'ongoing':
- return "merge_request[assignee_id]=" + gon.current_user_id;
+ return opts.fieldName + '[assignee_id]=' + gon.current_user_id;
case 'unassigned':
- return "merge_request[assignee_id]=";
+ return opts.fieldName + '[assignee_id]=';
case 'closed':
- return "merge_request[state_event]=close";
+ return opts.fieldName + '[state_event]=close';
}
})();
- if ($(ui.sender).data('state') === "closed") {
- data += "&merge_request[state_event]=reopen";
+ if (e.from.dataset.state === 'closed') {
+ data += '&' + opts.fieldName + '[state_event]=reopen';
}
- return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
+
+ opts.updateCallback(e.item, issuableUrl, data);
+ this.options.onUpdate.call(this, e);
}
- }).disableSelection();
+ });
};
return Milestone;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 7ab39ffbd05..2f08aa7fe8b 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -5,13 +5,20 @@
(function() {
this.MilestoneSelect = (function() {
- function MilestoneSelect(currentProject) {
- var _this;
+ function MilestoneSelect(currentProject, els) {
+ var _this, $els;
if (currentProject != null) {
_this = this;
this.currentProject = JSON.parse(currentProject);
}
- $('.js-milestone-select').each(function(i, dropdown) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-milestone-select');
+ }
+
+ $els.each(function(i, dropdown) {
var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
@@ -108,7 +115,7 @@
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
- var data, isIssueIndex, isMRIndex, page;
+ var data, isIssueIndex, isMRIndex, page, boardsStore;
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
@@ -116,9 +123,19 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
- gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
- gl.issueBoards.BoardsStore.updateFiltersUrl();
+
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') &&
+ !$dropdown.closest('.add-issues-modal').length) {
+ boardsStore = gl.issueBoards.BoardsStore.state.filters;
+ } else if ($dropdown.closest('.add-issues-modal').length) {
+ boardsStore = gl.issueBoards.ModalStore.store.filter;
+ }
+
+ if (boardsStore) {
+ boardsStore[$dropdown.data('field-name')] = selected.name;
+ if (!$dropdown.closest('.add-issues-modal').length) {
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ }
e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (selected.name != null) {
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
index 80549532ea9..919fcd0a07b 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6
@@ -21,8 +21,6 @@
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
-
- this.bindEvents();
}
/**
@@ -30,7 +28,7 @@
* All dropdown events are fired at the .dropdown-menu's parent element.
*/
bindEvents() {
- $(this.container).on('shown.bs.dropdown', this.getBuildsList);
+ $(document).on('shown.bs.dropdown', this.container, this.getBuildsList);
}
/**
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 2e6eb83cec7..b4491354472 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -2,13 +2,9 @@
/* global Network */
/* global ShortcutsNetwork */
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/));
(function() {
$(function() {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9db830a7ada..3579843baed 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,17 +1,17 @@
/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape */
/* global Flash */
-/* global GLForm */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
-/*= require autosave */
-/*= require autosize */
-/*= require dropzone */
-/*= require dropzone_input */
-/*= require gfm_auto_complete */
-/*= require jquery.atwho */
-/*= require task_list */
+require('./autosave');
+window.autosize = require('vendor/autosize');
+window.Dropzone = require('dropzone');
+require('./dropzone_input');
+require('./gfm_auto_complete');
+require('vendor/jquery.caret'); // required by jquery.atwho
+require('vendor/jquery.atwho');
+require('vendor/task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -420,7 +420,7 @@
Notes.prototype.setupNoteForm = function(form) {
var textarea;
- new GLForm(form);
+ new gl.GLForm(form);
textarea = form.find(".js-note-text");
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
@@ -455,7 +455,7 @@
var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
}
}
@@ -884,7 +884,7 @@
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
- new GLForm($editForm.find('form'));
+ new gl.GLForm($editForm.find('form'));
$editForm.find('form')
.attr('action', postUrl)
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index 43263368494..9203abefbbc 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
-//= require lib/utils/bootstrap_linked_tabs
+require('./lib/utils/bootstrap_linked_tabs');
((global) => {
class Pipelines {
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index f50802bdf2e..d7f3c9fd37e 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,7 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 7cf630a1d76..71719917d0c 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global Cookies */
-/* global Turbolinks */
/* global ProjectSelect */
(function() {
@@ -58,6 +57,11 @@
};
Project.prototype.initRefSwitcher = function() {
+ var refListItem = document.createElement('li');
+ var refLink = document.createElement('a');
+
+ refLink.href = '#';
+
return $('.js-project-refs-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
@@ -67,7 +71,8 @@
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
- ref: $dropdown.data('ref')
+ ref: $dropdown.data('ref'),
+ search: term
},
dataType: "json"
}).done(function(refs) {
@@ -76,16 +81,29 @@
},
selectable: true,
filterable: true,
+ filterRemote: true,
filterByText: true,
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
- var link;
+ var li = refListItem.cloneNode(false);
+
if (ref.header != null) {
- return $('<li />').addClass('dropdown-header').text(ref.header);
+ li.className = 'dropdown-header';
+ li.textContent = ref.header;
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
- return $('<li />').append(link);
+ var link = refLink.cloneNode(false);
+
+ if (ref === selected) {
+ link.className = 'is-active';
+ }
+
+ link.textContent = ref;
+ link.dataset.ref = ref;
+
+ li.appendChild(link);
}
+
+ return li;
},
id: function(obj, $el) {
return $el.attr('data-ref');
@@ -99,7 +117,7 @@
var $form = $dropdown.closest('form');
var action = $form.attr('action');
var divider = action.indexOf('?') < 0 ? '?' : '&';
- Turbolinks.visit(action + '' + divider + '' + $form.serialize());
+ gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
}
}
});
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index 6614d8952cd..d7943959238 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,11 +1,10 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */
-/* global Turbolinks */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
- return Turbolinks.visit(location.href);
+ return gl.utils.visitUrl(location.href);
}, 5000);
}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 03f4531abf5..5cf28aa7a73 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -49,7 +49,7 @@ class ProtectedBranchDropdown {
onClickCreateWildcard() {
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
- this.$dropdown.data('glDropdown').selectRowAtIndex(0);
+ this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getProtectedBranches(term, callback) {
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 15b3affd469..ffb66caf5f4 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1 +1,3 @@
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index 0caf8ba4344..bdbad93ad04 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -9,7 +9,7 @@
this.find('.js-render-math').renderMath();
};
- $(document).on('ready page:load', function() {
+ $(document).on('ready load', function() {
return $('body').renderGFM();
});
}).call(this);
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 489e567259c..b1c0dc37b4d 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -13,12 +13,12 @@
filterable: true,
fieldName: 'group_id',
search: {
- fields: ['name']
+ fields: ['full_name']
},
data: function(term, callback) {
return Api.groups(term, {}, function(data) {
data.unshift({
- name: 'Any'
+ full_name: 'Any'
});
data.splice(1, 0, 'divider');
return callback(data);
@@ -28,10 +28,10 @@
return obj.id;
},
text: function(obj) {
- return obj.name;
+ return obj.full_name;
},
toggleLabel: function(obj) {
- return ($groupDropdown.data('default-label')) + " " + obj.name;
+ return ($groupDropdown.data('default-label')) + " " + obj.full_name;
},
clicked: (function(_this) {
return function() {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index c56ee429b8e..c6d9b007ad1 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
-/* global Turbolinks */
/* global findFileURL */
(function() {
@@ -23,7 +22,7 @@
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() {
- return Turbolinks.visit(findFileURL);
+ return gl.utils.visitUrl(findFileURL);
});
}
}
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
deleted file mode 100644
index d50ddd98de1..00000000000
--- a/app/assets/javascripts/shortcuts_blob.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
-/* global Shortcuts */
-/* global Mousetrap */
-
-/*= require shortcuts */
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.ShortcutsBlob = (function(superClass) {
- extend(ShortcutsBlob, superClass);
-
- function ShortcutsBlob(skipResetBindings) {
- ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
- Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
- }
-
- ShortcutsBlob.copyToClipboard = function() {
- var clipboardButton;
- clipboardButton = $('.btn-clipboard');
- if (clipboardButton) {
- return clipboardButton.click();
- }
- };
-
- return ShortcutsBlob;
- })(Shortcuts);
-}).call(this);
diff --git a/app/assets/javascripts/shortcuts_blob.js.es6 b/app/assets/javascripts/shortcuts_blob.js.es6
new file mode 100644
index 00000000000..bfe90aef71e
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.js.es6
@@ -0,0 +1,29 @@
+/* global Mousetrap */
+/* global Shortcuts */
+
+require('./shortcuts');
+
+const defaults = {
+ skipResetBindings: false,
+ fileBlobPermalinkUrl: null,
+};
+
+class ShortcutsBlob extends Shortcuts {
+ constructor(opts) {
+ const options = Object.assign({}, defaults, opts);
+ super(options.skipResetBindings);
+ this.options = options;
+
+ Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
+ }
+
+ moveToFilePermalink() {
+ if (this.options.fileBlobPermalinkUrl) {
+ const hash = gl.utils.getLocationHash();
+ const hashUrlString = hash ? `#${hash}` : '';
+ gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
+ }
+ }
+}
+
+module.exports = ShortcutsBlob;
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 603fefbf15a..7378b322426 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 8469837533b..36e379d634d 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 4ef516af8c8..b841abb754d 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,11 +1,10 @@
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */
/* global Mousetrap */
-/* global Turbolinks */
/* global ShortcutsNavigation */
/* global sidebar */
-/*= require mousetrap */
-/*= require shortcuts_navigation */
+require('mousetrap');
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
@@ -39,17 +38,20 @@
}
ShortcutsIssuable.prototype.replyWithSelectedText = function() {
- var quote, replyField, documentFragment, selected, separator;
+ var quote, documentFragment, selected, separator;
+ var replyField = $('.js-main-target-form #note_note');
documentFragment = window.gl.utils.getSelectedFragment();
- if (!documentFragment) return;
+ if (!documentFragment) {
+ replyField.focus();
+ return;
+ }
// If the documentFragment contains more than just Markdown, don't copy as GFM.
if (documentFragment.querySelector('.md, .wiki')) return;
selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment);
- replyField = $('.js-main-target-form #note_note');
if (selected.trim() === "") {
return;
}
@@ -77,7 +79,7 @@
ShortcutsIssuable.prototype.editIssue = function() {
var $editBtn;
$editBtn = $('.issuable-edit');
- return Turbolinks.visit($editBtn.attr('href'));
+ return gl.utils.visitUrl($editBtn.attr('href'));
};
ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index afeda0dd5fe..cb5f2c53ea6 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global Shortcuts */
-/*= require shortcuts */
+require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index 79896e35cbb..651957f5325 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -2,7 +2,7 @@
/* global Mousetrap */
/* global ShortcutsNavigation */
-/*= require shortcuts_navigation */
+require('./shortcuts_navigation');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index 05234643c18..33e4b7db681 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,14 +1,12 @@
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
-((global) => {
- let singleton;
-
+(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
- const navbarSelector = '.navbar-fixed-top';
+ const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
@@ -23,11 +21,12 @@
class Sidebar {
constructor() {
- if (!singleton) {
- singleton = this;
- singleton.init();
+ if (!Sidebar.singleton) {
+ Sidebar.singleton = this;
+ Sidebar.singleton.init();
}
- return singleton;
+
+ return Sidebar.singleton;
}
init() {
@@ -36,13 +35,16 @@
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
+ $(window).on('resize', () => this.setSidebarHeight());
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
- .on('click', 'html, body', (e) => this.handleClickEvent(e))
- .on('page:change', () => this.renderState())
+ .on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
+ .on('DOMContentLoaded', () => this.renderState())
+ .on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
+ this.setSidebarHeight();
}
handleClickEvent(e) {
@@ -65,6 +67,16 @@
this.renderState();
}
+ setSidebarHeight() {
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const diff = $navHeight - $('body').scrollTop();
+ if (diff > 0) {
+ $('.js-right-sidebar').outerHeight($(window).height() - diff);
+ } else {
+ $('.js-right-sidebar').outerHeight('100%');
+ }
+ }
+
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
@@ -88,10 +100,12 @@
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
- setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ const sidebarContent = $(sidebarContentSelector);
+ setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
- global.Sidebar = Sidebar;
-})(window.gl || (window.gl = {}));
+ window.gl = window.gl || {};
+ gl.Sidebar = Sidebar;
+})();
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 5b20c63384c..3ee0c73a8d2 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -33,13 +33,13 @@
this.$toggleIcon.addClass('fa-caret-down');
}
- $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
+ $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff($(e.target));
}).bind(this));
}
SingleFileDiff.prototype.toggleDiff = function($target, cb) {
- if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
+ if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6
index 40f67637c7c..d1bdc353be2 100644
--- a/app/assets/javascripts/smart_interval.js.es6
+++ b/app/assets/javascripts/smart_interval.js.es6
@@ -89,7 +89,7 @@
destroy() {
this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
- $(document).off('visibilitychange').off('page:before-unload');
+ $(document).off('visibilitychange').off('beforeunload');
}
/* private */
@@ -111,8 +111,9 @@
}
initPageUnloadHandling() {
+ // TODO: Consider refactoring in light of turbolinks removal.
// prevent interval continuing after page change, when kept in cache by Turbolinks
- $(document).on('page:before-unload', () => this.cancel());
+ $(document).on('beforeunload', () => this.cancel());
}
handleVisibilityChange(e) {
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index cfb4ff82a73..64f9065be42 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,7 +1,9 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-/*= require_tree . */
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
(function() {
$(function() {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index b0132af70f2..e9e9aafd71a 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-/*= require ../blob/template_selector */
+require('../blob/template_selector');
((global) => {
class IssuableTemplateSelector extends gl.TemplateSelector {
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6
index 33d2c1e1a17..13cf3a10a38 100644
--- a/app/assets/javascripts/terminal/terminal_bundle.js.es6
+++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6
@@ -1,7 +1,7 @@
-//= require xterm/encoding-indexes
-//= require xterm/encoding
-//= require xterm/xterm.js
-//= require xterm/fit.js
-//= require ./terminal.js
+require('vendor/xterm/encoding-indexes.js');
+require('vendor/xterm/encoding.js');
+window.Terminal = require('vendor/xterm/xterm.js');
+require('vendor/xterm/fit.js');
+require('./terminal.js');
$(() => new gl.Terminal({ selector: '#terminal' }));
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index f05780167bf..7dba5840c8a 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -50,14 +50,15 @@
return (
children[target.index] ||
children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1]
+ children[target.index === 'last' ? children.length - 1 : -1] ||
+ el
);
}
function getRect(el) {
var rect = el.getBoundingClientRect();
var width = rect.right - rect.left;
- var height = rect.bottom - rect.top;
+ var height = rect.bottom - rect.top + 10;
return {
x: rect.left,
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index ef9c0a885fb..b07e62a8c30 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,6 +1,5 @@
/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */
/* global UsersSelect */
-/* global Turbolinks */
((global) => {
class Todos {
@@ -34,7 +33,7 @@
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
}
@@ -85,7 +84,7 @@
},
success: (data) => {
$target.remove();
- $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
+ $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
return this.updateBadges(data);
}
});
@@ -142,21 +141,33 @@
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
- return Turbolinks.visit(url);
+ return gl.utils.visitUrl(url);
}
}
goToTodoUrl(e) {
- const todoLink = $(this).data('url');
+ const todoLink = this.dataset.url;
+ let targetLink = e.target.getAttribute('href');
+
+ if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
+ targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
+ }
+
if (!todoLink) {
return;
}
- // Allow Meta-Click or Mouse3-click to open in a new tab
- if (e.metaKey || e.which === 2) {
+
+ if (gl.utils.isMetaClick(e)) {
e.preventDefault();
- return window.open(todoLink, '_blank');
+ // Meta-Click on username leads to different URL than todoLink.
+ // Turbolinks can resolve that URL, but window.open requires URL manually.
+ if (targetLink !== todoLink) {
+ return window.open(targetLink, '_blank');
+ } else {
+ return window.open(todoLink, '_blank');
+ }
} else {
- return Turbolinks.visit(todoLink);
+ return gl.utils.visitUrl(todoLink);
}
}
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index d124ca4f88b..b1b35fdbd6c 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,5 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
-/* global Turbolinks */
+
(function() {
this.TreeView = (function() {
function TreeView() {
@@ -15,7 +15,7 @@
e.preventDefault();
return window.open(path, '_blank');
} else {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
@@ -57,7 +57,7 @@
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
- return Turbolinks.visit(path);
+ return gl.utils.visitUrl(path);
}
}
});
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 313fb17aee8..465618e3d53 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -149,7 +149,6 @@ content on the Users#show page.
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
- turbolinks: true,
url: new_state
}, document.title, new_state);
return new_state;
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index e7280d643d3..6e40dfdf3d8 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,6 +1,5 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */
/* global d3 */
-/* global dateFormat */
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
@@ -33,7 +32,7 @@
date.setDate(date.getDate() + i);
var day = date.getDay();
- var count = timestamps[dateFormat(date, 'yyyy-mm-dd')];
+ var count = timestamps[date.format('yyyy-mm-dd')];
// Create a new group array if this is the first day of the week
// or if is first object
@@ -122,7 +121,7 @@
if (stamp.count > 0) {
contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
}
- dateText = dateFormat(date, 'mmm d, yyyy');
+ dateText = date.format('mmm d, yyyy');
return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
};
})(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index f50802bdf2e..4cad60a59b1 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,7 +1,3 @@
-/* eslint-disable func-names, space-before-function-paren */
-
-/*= require_tree . */
-
-(function() {
-
-}).call(this);
+// require everything else in this directory
+function requireAll(context) { return context.keys().map(context); }
+requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 77d2764cdf0..d4b24d13299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -8,7 +8,8 @@
slice = [].slice;
this.UsersSelect = (function() {
- function UsersSelect(currentUser) {
+ function UsersSelect(currentUser, els) {
+ var $els;
this.users = bind(this.users, this);
this.user = bind(this.user, this);
this.usersPath = "/autocomplete/users.json";
@@ -20,7 +21,14 @@
this.currentUser = JSON.parse(currentUser);
}
}
- $('.js-user-search').each((function(_this) {
+
+ $els = $(els);
+
+ if (!els) {
+ $els = $('.js-user-search');
+ }
+
+ $els.each((function(_this) {
return function(i, dropdown) {
var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
@@ -193,7 +201,9 @@
selectedId = user.id;
return;
}
- if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.closest('.add-issues-modal').length) {
+ gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
+ } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6
index edd01f17a97..e7432afb56e 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6
@@ -1,42 +1,36 @@
+/* eslint-disable no-param-reassign */
/* global Vue, VueResource, gl */
-/*= require vue_common_component/commit */
-/*= require vue_pagination/index */
-/*= require vue-resource
-/*= require boards/vue_resource_interceptor */
-/*= require ./status.js.es6 */
-/*= require ./store.js.es6 */
-/*= require ./pipeline_url.js.es6 */
-/*= require ./stage.js.es6 */
-/*= require ./stages.js.es6 */
-/*= require ./pipeline_actions.js.es6 */
-/*= require ./time_ago.js.es6 */
-/*= require ./pipelines.js.es6 */
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('../lib/utils/common_utils');
+require('../vue_shared/vue_resource_interceptor');
+require('./pipelines');
-(() => {
- const project = document.querySelector('.pipelines');
- const entry = document.querySelector('.vue-pipelines-index');
- const svgs = document.querySelector('.pipeline-svgs');
+$(() => new Vue({
+ el: document.querySelector('.vue-pipelines-index'),
- Vue.use(VueResource);
+ data() {
+ const project = document.querySelector('.pipelines');
+ const svgs = document.querySelector('.pipeline-svgs').dataset;
- if (!entry) return null;
- return new Vue({
- el: entry,
- data: {
+ // Transform svgs DOMStringMap to a plain Object.
+ const svgsObject = gl.utils.DOMStringMapToObject(svgs);
+
+ return {
scope: project.dataset.url,
store: new gl.PipelineStore(),
- svgs: svgs.dataset,
- },
- components: {
- 'vue-pipelines': gl.VuePipelines,
- },
- template: `
- <vue-pipelines
- :scope='scope'
- :store='store'
- :svgs='svgs'
- >
- </vue-pipelines>
- `,
- });
-})();
+ svgs: svgsObject,
+ };
+ },
+ components: {
+ 'vue-pipelines': gl.VuePipelines,
+ },
+ template: `
+ <vue-pipelines
+ :scope='scope'
+ :store='store'
+ :svgs='svgs'
+ >
+ </vue-pipelines>
+ `,
+}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index b195b0ef3ba..8106934e864 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -26,10 +26,9 @@
v-if='actions'
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
- title="Manual build"
+ title="Manual job"
data-placement="top"
- data-toggle="dropdown"
- aria-label="Manual build"
+ aria-label="Manual job"
>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
@@ -51,7 +50,6 @@
<button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- data-toggle="dropdown"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
@@ -83,8 +81,7 @@
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
- aria-label="Retry"
- >
+ aria-label="Retry">
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
@@ -96,8 +93,7 @@
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
- aria-label="Cancel"
- >
+ aria-label="Cancel">
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
index b2ed05503c9..e47dc6935d6 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6
@@ -1,19 +1,19 @@
-/* global Vue, Turbolinks, gl */
+/* global Vue, gl */
/* eslint-disable no-param-reassign */
+window.Vue = require('vue');
+require('../vue_shared/components/table_pagination');
+require('./store');
+require('../vue_shared/components/pipelines_table');
+
((gl) => {
gl.VuePipelines = Vue.extend({
+
components: {
- runningPipeline: gl.VueRunningPipeline,
- pipelineActions: gl.VuePipelineActions,
- stages: gl.VueStages,
- commit: gl.CommitComponent,
- pipelineUrl: gl.VuePipelineUrl,
- pipelineHead: gl.VuePipelineHead,
- glPagination: gl.VueGlPagination,
- statusScope: gl.VueStatusScope,
- timeAgo: gl.VueTimeAgo,
+ 'gl-pagination': gl.VueGlPagination,
+ 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
},
+
data() {
return {
pipelines: [],
@@ -36,89 +36,31 @@
},
methods: {
change(pagenum, apiScope) {
- Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
- },
- author(pipeline) {
- if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
- if (pipeline.commit.author) return pipeline.commit.author;
- return {
- avatar_url: pipeline.commit.author_gravatar_url,
- web_url: `mailto:${pipeline.commit.author_email}`,
- username: pipeline.commit.author_name,
- };
- },
- ref(pipeline) {
- const { ref } = pipeline;
- return { name: ref.name, tag: ref.tag, ref_url: ref.path };
- },
- commitTitle(pipeline) {
- return pipeline.commit ? pipeline.commit.title : '';
- },
- commitSha(pipeline) {
- return pipeline.commit ? pipeline.commit.short_id : '';
- },
- commitUrl(pipeline) {
- return pipeline.commit ? pipeline.commit.commit_path : '';
- },
- match(string) {
- return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
},
template: `
<div>
- <div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
+ <div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i>
</div>
- <div class="table-holder" v-if='pipelines.length'>
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="pipeline-status">Status</th>
- <th class="pipeline-info">Pipeline</th>
- <th class="pipeline-commit">Commit</th>
- <th class="pipeline-stages">Stages</th>
- <th class="pipeline-date"></th>
- <th class="pipeline-actions hidden-xs"></th>
- </tr>
- </thead>
- <tbody>
- <tr class="commit" v-for='pipeline in pipelines'>
- <status-scope
- :pipeline='pipeline'
- :match='match'
- :svgs='svgs'
- >
- </status-scope>
- <pipeline-url :pipeline='pipeline'></pipeline-url>
- <td>
- <commit
- :commit-icon-svg='svgs.commitIconSvg'
- :author='author(pipeline)'
- :tag="pipeline.ref.tag"
- :title='commitTitle(pipeline)'
- :commit-ref='ref(pipeline)'
- :short-sha='commitSha(pipeline)'
- :commit-url='commitUrl(pipeline)'
- >
- </commit>
- </td>
- <stages
- :pipeline='pipeline'
- :svgs='svgs'
- :match='match'
- >
- </stages>
- <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
- <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
- </tr>
- </tbody>
- </table>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
</div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
+
+ <div class="table-holder" v-if='!pageRequest && pipelines.length'>
+ <pipelines-table-component
+ :pipelines='pipelines'
+ :svgs='svgs'>
+ </pipelines-table-component>
</div>
+
<gl-pagination
- v-if='pageInfo.total > pageInfo.perPage'
+ v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 496df9aaced..8cc417a9966 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -15,7 +15,7 @@
required: true,
},
svgs: {
- type: DOMStringMap,
+ type: Object,
required: true,
},
match: {
diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6
deleted file mode 100644
index cb176b3f0c6..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6
+++ /dev/null
@@ -1,21 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueStages = Vue.extend({
- components: {
- 'vue-stage': gl.VueStage,
- },
- props: ['pipeline', 'svgs', 'match'],
- template: `
- <td class="stage-cell">
- <div
- class="stage-container dropdown js-mini-pipeline-graph"
- v-for='stage in pipeline.details.stages'
- >
- <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 9e19b1564dc..0ee21f00fdc 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -1,6 +1,6 @@
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
-/*= require vue_realtime_listener/index.js */
+require('../vue_realtime_listener');
((gl) => {
const pageValues = (headers) => {
@@ -20,6 +20,7 @@
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
+ this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
@@ -41,16 +42,18 @@
this.pageRequest = false;
}, () => {
this.pageRequest = false;
- return new Flash('Something went wrong on our end.');
+ return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
- this.$children
- .filter(e => e.$options._componentTag === 'time-ago')
- .forEach(e => e.changeTime());
+ this.$children[0].$children.reduce((acc, component) => {
+ const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
+ acc.push(timeAgoComponent);
+ return acc;
+ }, []).forEach(e => e.changeTime());
}, 10000);
};
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
index 655110feba1..3598da11573 100644
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -1,6 +1,9 @@
/* global Vue, gl */
/* eslint-disable no-param-reassign */
+window.Vue = require('vue');
+require('../lib/utils/datetime_utility');
+
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6
index 23cac1466d2..30f6680a673 100644
--- a/app/assets/javascripts/vue_realtime_listener/index.js.es6
+++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6
@@ -7,12 +7,23 @@
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
- document.removeEventListener('page:fetch', removeAll);
+ document.removeEventListener('beforeunload', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
- document.addEventListener('page:fetch', removeAll);
+ document.addEventListener('beforeunload', removeAll);
+
+ // add removeAll methods to stack
+ const stack = gl.VueRealtimeListener.reset;
+ gl.VueRealtimeListener.reset = () => {
+ gl.VueRealtimeListener.reset = stack;
+ removeAll();
+ stack();
+ };
};
+
+ // remove all event listeners and intervals
+ gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6
index 62a22e39a3b..7f7c18ddeb1 100644
--- a/app/assets/javascripts/vue_common_component/commit.js.es6
+++ b/app/assets/javascripts/vue_shared/components/commit.js.es6
@@ -1,5 +1,5 @@
-/*= require vue */
/* global Vue */
+
(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
new file mode 100644
index 00000000000..4bdaef31ee9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6
@@ -0,0 +1,61 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('./pipelines_table_row');
+/**
+ * Pipelines Table Component.
+ *
+ * Given an array of objects, renders a table.
+ */
+
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
+
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack.
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ },
+
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions hidden-xs"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :svgs="svgs"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
new file mode 100644
index 00000000000..61c1b72d9d2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6
@@ -0,0 +1,234 @@
+/* eslint-disable no-param-reassign */
+/* global Vue */
+
+require('../../vue_pipelines_index/status');
+require('../../vue_pipelines_index/pipeline_url');
+require('../../vue_pipelines_index/stage');
+require('../../vue_pipelines_index/pipeline_actions');
+require('../../vue_pipelines_index/time_ago');
+require('./commit');
+/**
+ * Pipeline table row.
+ *
+ * Given the received object renders a table row in the pipelines' table.
+ */
+(() => {
+ window.gl = window.gl || {};
+ gl.pipelines = gl.pipelines || {};
+
+ gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
+
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+
+ /**
+ * TODO: Remove this when we have webpack;
+ */
+ svgs: {
+ type: Object,
+ required: true,
+ default: () => ({}),
+ },
+ },
+
+ components: {
+ 'commit-component': gl.CommitComponent,
+ 'pipeline-actions': gl.VuePipelineActions,
+ 'dropdown-stage': gl.VueStage,
+ 'pipeline-url': gl.VuePipelineUrl,
+ 'status-scope': gl.VueStatusScope,
+ 'time-ago': gl.VueTimeAgo,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
+ },
+
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
+ }
+ return accumulator;
+ }, {});
+ }
+
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
+ },
+
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ methods: {
+ /**
+ * FIXME: This should not be in this component but in the components that
+ * need this function.
+ *
+ * Used to render SVGs in the following components:
+ * - status-scope
+ * - dropdown-stage
+ *
+ * @param {String} string
+ * @return {String}
+ */
+ match(string) {
+ return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope
+ :pipeline="pipeline"
+ :svgs="svgs"
+ :match="match">
+ </status-scope>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"
+ :commit-icon-svg="svgs.commitIconSvg">
+ </commit-component>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage
+ :stage="stage"
+ :svgs="svgs"
+ :match="match">
+ </dropdown-stage>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago>
+
+ <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions>
+ </tr>
+ `,
+ });
+})();
diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
index 605824fa939..67c6cb73761 100644
--- a/app/assets/javascripts/vue_pagination/index.js.es6
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6
@@ -1,6 +1,8 @@
/* global Vue, gl */
/* eslint-disable no-param-reassign, no-plusplus */
+window.Vue = require('vue');
+
((gl) => {
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
@@ -13,6 +15,8 @@
gl.VueGlPagination = Vue.extend({
props: {
+ // TODO: Consider refactoring in light of turbolinks removal.
+
/**
This function will take the information given by the pagination component
And make a new Turbolinks call
@@ -20,7 +24,7 @@
Here is an example `change` method:
change(pagenum, apiScope) {
- Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
+ gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
},
*/
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..d3229f9f730
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6
@@ -0,0 +1,23 @@
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
+no-param-reassign, no-plusplus */
+/* 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);
+ }
+
+ Vue.activeResources--;
+ });
+});
+
+Vue.http.interceptors.push((request, next) => {
+ // needed in order to not break the tests.
+ if ($.rails) {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+ next();
+});
diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6
index ecff5fd5bf4..ef99b2e92f0 100644
--- a/app/assets/javascripts/wikis.js.es6
+++ b/app/assets/javascripts/wikis.js.es6
@@ -1,9 +1,9 @@
/* eslint-disable no-param-reassign */
/* global Breakpoints */
-/*= require latinise */
-/*= require breakpoints */
-/*= require jquery.nicescroll */
+require('vendor/latinise');
+require('./breakpoints');
+require('vendor/jquery.nicescroll');
((global) => {
const dasherize = str => str.replace(/[_\s]+/g, '-');
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index a8b7be7ad06..d9261cda1b1 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -6,11 +6,11 @@
//
/*= provides zen_mode:enter */
/*= provides zen_mode:leave */
-//
-/*= require jquery.scrollTo */
-/*= require dropzone */
-/*= require mousetrap */
-/*= require mousetrap/pause */
+
+require('vendor/jquery.scrollTo');
+window.Dropzone = require('dropzone');
+require('mousetrap');
+require('mousetrap/plugins/pause/mousetrap-pause');
//
// ### Events
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 8b93665d085..1dcd1f8a6fc 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery-ui/datepicker
*= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
@@ -19,6 +18,8 @@
* directory.
*/
+@import "../../../node_modules/pikaday/scss/pikaday";
+
/*
* GitLab UI framework
*/
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 3cf49f4ff1b..08f203a1bf6 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -31,7 +31,6 @@
@import "framework/modal.scss";
@import "framework/nav.scss";
@import "framework/pagination.scss";
-@import "framework/progress.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 8d38fc78a19..0ca5a9343f7 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -71,6 +71,27 @@
transition: $unfoldedTransitions;
}
+@mixin disableAllAnimation {
+ /*CSS transitions*/
+ -o-transition-property: none !important;
+ -moz-transition-property: none !important;
+ -ms-transition-property: none !important;
+ -webkit-transition-property: none !important;
+ transition-property: none !important;
+ /*CSS transforms*/
+ -o-transform: none !important;
+ -moz-transform: none !important;
+ -ms-transform: none !important;
+ -webkit-transform: none !important;
+ transform: none !important;
+ /*CSS animations*/
+ -webkit-animation: none !important;
+ -moz-animation: none !important;
+ -o-animation: none !important;
+ -ms-animation: none !important;
+ animation: none !important;
+}
+
@function unfoldTransition ($transition) {
// Default values
$property: all;
@@ -107,8 +128,7 @@
.note-action-button .link-highlight,
.toolbar-btn,
-.dropdown-toggle-caret,
-.fa:not(.fa-bell) {
+.dropdown-toggle-caret {
@include transition(color);
}
@@ -116,11 +136,13 @@ a {
@include transition(background-color, color, border);
}
-.tree-table td,
-.well-list > li {
- @include transition(background-color, border-color);
-}
-
.stage-nav-item {
@include transition(background-color, box-shadow);
}
+
+.nav-sidebar a,
+.dropdown-menu a,
+.dropdown-menu button,
+.dropdown-menu-nav a {
+ transition: none;
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 1d59700543c..3f5b78ed445 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -28,6 +28,8 @@
.avatar {
@extend .avatar-circle;
+ @include transition-property(none);
+
width: 40px;
height: 40px;
padding: 0;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 592ef0d647f..0f9213b98e3 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -278,6 +278,10 @@
display: inline-block;
}
+ .btn {
+ margin: $btn-side-margin $btn-side-margin 0 0;
+ }
+
@media(max-width: $screen-xs-max) {
margin-top: 50px;
text-align: center;
@@ -286,6 +290,12 @@
width: 100%;
}
}
+
+ @media(min-width: $screen-xs-max) {
+ &.labels .text-content {
+ margin-top: 70px;
+ }
+ }
}
.flex-container-block {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index bb6129158d9..cda46223492 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -330,10 +330,6 @@
}
}
-.btn-file-option {
- background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 1d2d1bfc0d7..fb8ea18d122 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -9,6 +9,8 @@
}
.user-calendar-activities {
+ direction: ltr;
+
.str-truncated {
max-width: 70%;
}
@@ -43,3 +45,56 @@
float: right;
font-size: 12px;
}
+
+.pika-single.gitlab-theme {
+ .pika-label {
+ color: $gl-text-color-secondary;
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+ th {
+ padding: 2px 0;
+ color: $note-disabled-comment-color;
+ font-weight: normal;
+ text-transform: lowercase;
+ border-top: 1px solid $calendar-border-color;
+ }
+
+ abbr {
+ cursor: default;
+ }
+
+ td {
+ border: 1px solid $calendar-border-color;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+ }
+
+ .pika-day {
+ border-radius: 0;
+ background-color: $white-light;
+ text-align: center;
+ }
+
+ .is-today {
+ .pika-day {
+ color: inherit;
+ font-weight: normal;
+ }
+ }
+
+ .is-selected .pika-day,
+ .pika-day:hover,
+ .is-today .pika-day:hover {
+ background: $gl-primary;
+ color: $white-light;
+ box-shadow: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 0ce94a26a7f..a4b38723bbd 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -253,6 +253,8 @@ li.note {
.progress {
margin-bottom: 0;
margin-top: 4px;
+ box-shadow: none;
+ background-color: $border-gray-light;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 6bfb9a6d1cb..ff31e7f7b3d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -125,7 +125,6 @@
top: 100%;
left: 0;
z-index: 9;
- max-width: 280px;
min-width: 240px;
margin-top: 2px;
margin-bottom: 0;
@@ -137,6 +136,10 @@
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
+ .filtered-search-input-container & {
+ max-width: 280px;
+ }
+
&.is-loading {
.dropdown-content {
display: none;
@@ -227,6 +230,11 @@
}
}
+.dropdown-menu-drop-up {
+ top: auto;
+ bottom: 100%;
+}
+
.dropdown-menu-large {
width: 340px;
}
@@ -497,119 +505,16 @@
max-height: 230px;
}
- .ui-widget {
- table {
- margin: 0;
- }
-
- &.ui-datepicker-inline {
- padding: 0 10px;
- border: 0;
- width: 100%;
- }
-
- .ui-datepicker-header {
- padding: 0 8px 10px;
- border: 0;
-
- .ui-icon {
- background: none;
- font-size: 20px;
- text-indent: 0;
-
- &::before {
- display: block;
- position: relative;
- top: -2px;
- color: $dropdown-title-btn-color;
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- }
- }
-
- .ui-datepicker-calendar {
- .ui-state-hover,
- .ui-state-active {
- color: $white-light;
- border: 0;
- }
- }
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 0;
- height: 15px;
- cursor: pointer;
-
- &:hover {
- background-color: transparent;
- border: 0;
-
- .ui-icon::before {
- color: $md-link-color;
- }
- }
- }
-
- .ui-datepicker-prev {
- left: 0;
-
- .ui-icon::before {
- content: '\f104';
- text-align: left;
- }
- }
-
- .ui-datepicker-next {
- right: 0;
-
- .ui-icon::before {
- content: '\f105';
- text-align: right;
- }
- }
-
- td {
- padding: 0;
- border: 1px solid $calendar-border-color;
-
- &:first-child {
- border-left: 0;
- }
-
- &:last-child {
- border-right: 0;
- }
-
- a {
- line-height: 17px;
- border: 0;
- border-radius: 0;
- }
- }
-
- .ui-datepicker-title {
- color: $gl-text-color;
- font-size: 14px;
- line-height: 1;
- font-weight: normal;
- }
- }
-
- th {
- padding: 2px 0;
- color: $note-disabled-comment-color;
- font-weight: normal;
- text-transform: lowercase;
- border-top: 1px solid $calendar-border-color;
+ .pika-single {
+ position: relative!important;
+ top: 0!important;
+ border: 0;
+ box-shadow: none;
}
- .ui-datepicker-unselectable {
- background-color: $gray-light;
+ .pika-lendar {
+ margin-top: -5px;
+ margin-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c51912b4ac4..30f242a35db 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -231,3 +231,46 @@ span.idiff {
}
}
}
+
+.file-title-flex-parent {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background-color: $gray-light;
+ border-bottom: 1px solid $border-color;
+ padding: 5px $gl-padding;
+ margin: 0;
+ border-radius: 3px 3px 0 0;
+
+ .file-header-content {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 30px;
+ position: relative;
+ }
+
+ .btn-clipboard {
+ position: absolute;
+ right: 0;
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+
+ small {
+ margin: 0 10px 0 0;
+ }
+
+ .file-actions {
+ white-space: nowrap;
+
+ .btn {
+ padding: 0 10px;
+ font-size: 13px;
+ line-height: 28px;
+ display: inline-block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 2a01bc4d44d..34e010e0e8a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -222,6 +222,10 @@ header {
float: right;
border-top: none;
+ @media (min-width: $screen-md-min) {
+ padding: 0;
+ }
+
@media (max-width: $screen-xs-max) {
float: none;
}
@@ -272,7 +276,7 @@ header {
.header-user {
.dropdown-menu-nav {
- width: 140px;
+ min-width: 140px;
margin-top: -5px;
}
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 868f28cd356..db8d231a82a 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -58,3 +58,9 @@
fill: $gl-text-color;
}
}
+
+.icon-link {
+ &:hover {
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss
index 18f2f316f02..d335fedefe2 100644
--- a/app/assets/stylesheets/framework/jquery.scss
+++ b/app/assets/stylesheets/framework/jquery.scss
@@ -2,42 +2,6 @@
font-family: $regular_font;
font-size: $font-size-base;
- &.ui-datepicker,
- &.ui-datepicker-inline {
- border: 1px solid $jq-ui-border;
- padding: 10px;
- width: 270px;
-
- .ui-datepicker-header {
- background: $white-light;
- border-color: $jq-ui-border;
-
- .ui-datepicker-prev,
- .ui-datepicker-next {
- top: 4px;
- }
-
- .ui-datepicker-prev {
- left: 2px;
- }
-
- .ui-datepicker-next {
- right: 2px;
- }
-
- .ui-state-hover {
- background: transparent;
- border: 0;
- cursor: pointer;
- }
- }
-
- .ui-datepicker-calendar td a {
- padding: 5px;
- text-align: center;
- }
- }
-
&.ui-autocomplete {
border-color: $jq-ui-border;
padding: 0;
@@ -59,25 +23,4 @@
border: 0;
background: transparent;
}
-
- .ui-datepicker-calendar {
- .ui-state-active,
- .ui-state-hover,
- .ui-state-focus {
- border: 1px solid $gl-primary;
- background: $gl-primary;
- color: $white-light;
- }
- }
-}
-
-.ui-sortable-handle {
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 426596027de..2bfdb9f9601 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -307,3 +307,7 @@ ul.controls {
}
}
}
+
+ul.indent-list {
+ padding: 10px 0 0 30px;
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5bff694658c..d4758d90352 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -159,6 +159,7 @@
.cur {
.avatar {
border: 1px solid $white-light;
+ @include disableAllAnimation;
}
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 401c2d0f6ee..674d3bb45aa 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -283,10 +283,7 @@
}
.layout-nav {
- position: fixed;
- top: $header-height;
width: 100%;
- z-index: 11;
background: $gray-light;
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
@@ -294,16 +291,18 @@
.container-fluid {
position: relative;
+
+ .nav-control {
+ @media (max-width: $screen-sm-max) {
+ margin-right: 75px;
+ }
+ }
}
.controls {
float: right;
padding: 7px 0 0;
- @media (max-width: $screen-sm-max) {
- display: none;
- }
-
i {
color: $layout-link-gray;
}
@@ -361,6 +360,7 @@
.fade-left {
@include fade(right, $gray-light);
left: -5px;
+ text-align: center;
.fa {
left: -7px;
@@ -416,15 +416,20 @@
}
.page-with-layout-nav {
- margin-top: $header-height + 2;
-
.right-sidebar {
top: ($header-height * 2) + 2;
}
+
+ .build-sidebar {
+ top: ($header-height * 3) + 3;
+
+ &.affix {
+ top: 0;
+ }
+ }
}
.activities {
-
.nav-block {
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b37c1d0d670..c3ec9db0f07 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -6,8 +6,22 @@
.pagination {
padding: 0;
+
+ a {
+ cursor: pointer;
+ }
+
+ .separator,
+ .separator:hover {
+ a {
+ cursor: default;
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ }
+ }
}
+
.gap,
.gap:hover {
background-color: $gray-light;
diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss
deleted file mode 100644
index e9800bd24b5..00000000000
--- a/app/assets/stylesheets/framework/progress.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-html.turbolinks-progress-bar::before {
- background-color: $progress-color!important;
- height: 2px!important;
- box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color;
-}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index f0b03710c79..20bcb1eeb23 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,5 +1,5 @@
.page-with-sidebar {
- padding: $header-height 0 25px;
+ padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
@@ -208,7 +208,9 @@ header.header-sidebar-pinned {
padding-right: 0;
@media (min-width: $screen-sm-min) {
- padding-right: $sidebar_collapsed_width;
+ .content-wrapper {
+ padding-right: $sidebar_collapsed_width;
+ }
.merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
@@ -234,7 +236,9 @@ header.header-sidebar-pinned {
}
@media (min-width: $screen-md-min) {
- padding-right: $gutter_width;
+ .content-wrapper {
+ padding-right: $gutter_width;
+ }
&:not(.with-overlay) .merge-request-tabs-holder.affix {
right: $gutter_width;
@@ -252,4 +256,9 @@ header.header-sidebar-pinned {
.right-sidebar {
border-left: 1px solid $border-color;
+
+ &.affix {
+ position: fixed;
+ top: 0;
+ }
}
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 12d56359d7d..ea2d26dd5a0 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -162,6 +162,10 @@
}
}
}
+
+ &.panel-without-border {
+ border: 0;
+ }
}
.panel-succes .panel-heading,
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 60ff72c703e..ea40f449134 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -138,6 +138,13 @@ pre {
margin: 0;
}
+blockquote {
+ color: $gl-grayish-blue;
+ padding: 0 0 0 15px;
+ margin: 0;
+ border-left: 3px solid $white-dark;
+}
+
span.highlight_word {
background-color: $highlighted-highlight-word !important;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index f2d60bff2b5..9a36d76136b 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -250,7 +250,7 @@
}
.issue-boards-search {
- width: 290px;
+ width: 395px;
.form-control {
display: inline-block;
@@ -298,12 +298,8 @@
.issue-boards-sidebar {
&.right-sidebar {
- top: 153px;
+ top: 0;
bottom: 0;
-
- @media (min-width: $screen-sm-min) {
- top: 220px;
- }
}
.issuable-sidebar-header {
@@ -354,3 +350,171 @@
padding-right: 0;
}
}
+
+.add-issues-modal {
+ display: -webkit-flex;
+ display: flex;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ background-color: rgba($black, .3);
+ z-index: 9999;
+}
+
+.add-issues-container {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 90vw;
+ height: 85vh;
+ max-width: 1100px;
+ min-height: 500px;
+ margin: auto;
+ padding: 25px 15px 0;
+ background-color: $white-light;
+ border-radius: $border-radius-default;
+ box-shadow: 0 2px 12px rgba($black, .5);
+
+ .empty-state {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ margin-top: 0;
+
+ &.add-issues-empty-state-filter {
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ -webkit-justify-content: center;
+ justify-content: center;
+ }
+
+ > .row {
+ width: 100%;
+ margin: auto 0;
+ }
+
+ .svg-content {
+ margin-top: -40px;
+ }
+ }
+}
+
+.add-issues-header {
+ margin: -25px -15px -5px;
+ border-top: 0;
+ border-bottom: 1px solid $border-color;
+ border-top-right-radius: $border-radius-default;
+ border-top-left-radius: $border-radius-default;
+
+ > h2 {
+ margin: 0;
+ font-size: 18px;
+ }
+}
+
+.add-issues-search {
+ display: -webkit-flex;
+ display: flex;
+
+ .form-control {
+ margin-left: auto;
+
+ @media (min-width: $screen-sm-min) {
+ max-width: 200px;
+ }
+ }
+}
+
+.add-issues-list-column {
+ width: 100%;
+
+ @media (min-width: $screen-sm-min) {
+ width: 50%;
+ }
+
+ @media (min-width: $screen-md-min) {
+ width: (100% / 3);
+ }
+}
+
+.add-issues-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ padding-top: 3px;
+ margin-left: -$gl-vert-padding;
+ margin-right: -$gl-vert-padding;
+ overflow-y: scroll;
+
+ .card-parent {
+ padding: 0 5px 5px;
+ }
+
+ .card {
+ border: 1px solid $border-gray-dark;
+ box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3);
+ cursor: pointer;
+ }
+}
+
+.add-issues-list-loading {
+ -webkit-align-self: center;
+ align-self: center;
+ width: 100%;
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ font-size: 35px;
+}
+
+.add-issues-footer {
+ margin: auto -15px 0;
+ padding-left: 15px;
+ padding-right: 15px;
+ border-bottom-right-radius: $border-radius-default;
+ border-bottom-left-radius: $border-radius-default;
+}
+
+.add-issues-footer-to-list {
+ padding-left: $gl-vert-padding;
+ padding-right: $gl-vert-padding;
+ line-height: 34px;
+}
+
+.issue-card-selected {
+ position: absolute;
+ right: -3px;
+ top: -3px;
+ width: 17px;
+ background-color: $blue-light;
+ color: $white-light;
+ border: 1px solid $border-blue-light;
+ font-size: 9px;
+ line-height: 15px;
+ border-radius: 50%;
+}
+
+.modal-filters {
+ display: flex;
+
+ > .dropdown {
+ display: none;
+ margin-right: 10px;
+
+ @media (min-width: $screen-sm-min) {
+ display: block;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100px;
+
+ @media (min-width: $screen-md-min) {
+ width: 140px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index fef8e8eec27..c3d45d708c1 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -159,7 +159,6 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid $white-normal;
padding: 10px 15px;
margin: 10px 0;
background: $gray-light;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index cda069e6c0e..5b777953fb0 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -284,7 +284,11 @@
.events-description {
line-height: 65px;
- padding-left: $gl-padding;
+ padding: 0 $gl-padding;
+ }
+
+ .events-info {
+ color: $gl-text-color-secondary;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 96ba7c40634..92d7772da57 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -34,9 +34,14 @@
}
}
- .file-title {
+ .file-title,
+ .file-title-flex-parent {
cursor: pointer;
+ a:hover {
+ text-decoration: none;
+ }
+
&:hover {
background-color: $gray-normal;
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index b989d72ce1c..5776d86983a 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -41,7 +41,6 @@
word-wrap: break-word;
.md {
- color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 4ef95d27f4f..da5c44b5fdc 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -189,11 +189,11 @@
}
.right-sidebar {
- position: fixed;
+ position: absolute;
top: $header-height;
bottom: 0;
right: 0;
- z-index: 10;
+ z-index: 8;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -461,8 +461,19 @@
.issuable-list {
li {
+
+ .issue-box {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ padding-right: $gl-padding;
+ }
+
.issue-check {
- float: left;
padding-right: $gl-padding;
margin-bottom: 10px;
min-width: 15px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8734a3b1598..80b0c9493d8 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,6 +1,6 @@
.issues-list {
.issue {
- padding: 10px $gl-padding;
+ padding: 10px 0 10px $gl-padding;
position: relative;
.title {
@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal;
}
}
+
+.recaptcha {
+ margin-bottom: 30px;
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 21d9b4c54ea..e1ef0b029a5 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,6 +116,22 @@
}
.manage-labels-list {
+ > li:not(.empty-message) {
+ background-color: $white-light;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+
+ &.sortable-ghost {
+ opacity: 0.3;
+ }
+ }
+
.btn-action {
color: $gl-text-color;
@@ -259,3 +275,8 @@
}
}
}
+
+.label-link {
+ display: inline-block;
+ vertical-align: text-top;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index ab68b360f93..0b0c4bc130d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -56,34 +56,52 @@
&.right {
float: right;
padding-right: 0;
+ }
- a {
- color: $gl-text-color;
- }
+ .modify-merge-commit-link {
+ color: $gl-text-color;
}
- .remove_source_checkbox {
+ .merge-param-checkbox {
margin: 0;
}
+
+ a .fa-question-circle {
+ color: $gl-text-color-secondary;
+
+ &:hover,
+ &:focus {
+ color: $link-hover-color;
+ }
+ }
}
}
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-align-items: center;
+ align-items: center;
+
+ i,
+ svg {
+ margin-right: 8px;
+ }
svg {
- margin-right: 4px;
position: relative;
top: 1px;
overflow: visible;
}
- &.ci-success_with_warnings {
+ & > span {
+ padding-right: 4px;
+ }
- i {
- color: $gl-warning;
- }
+ @media (max-width: $screen-xs-max) {
+ flex-wrap: wrap;
}
}
@@ -93,6 +111,43 @@
padding: $gl-padding;
}
+ .mr-widget-pipeline-graph {
+ flex-shrink: 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+
+ .ci-action-icon-wrapper {
+ line-height: 16px;
+ }
+
+ @media (min-width: $screen-sm-min) {
+ .stage-cell {
+ padding: 0 4px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ order: 1;
+ margin-top: $gl-padding-top;
+ border-radius: 3px;
+ background-color: $white-light;
+ border: 1px solid $gray-darker;
+ width: 100%;
+ text-align: center;
+
+ .dropdown-menu {
+ margin-left: -97.5px;
+ }
+
+ .arrow-up::before,
+ .arrow-up::after, {
+ margin-left: 97.5px;
+ }
+ }
+ }
+
.normal {
color: $gl-text-color;
}
@@ -214,8 +269,15 @@
.mr-list {
.merge-request {
- padding: 10px 15px;
+ padding: 10px 0 10px 15px;
position: relative;
+ display: -webkit-flex;
+ display: flex;
+
+ .issue-info-container {
+ -webkit-flex: 1;
+ flex: 1;
+ }
.merge-request-title {
margin-bottom: 2px;
@@ -421,7 +483,7 @@
background-color: $white-light;
&.affix {
- top: 100px;
+ top: 0;
left: 0;
z-index: 10;
transition: right .15s;
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 686b64cdd24..3da1150f89b 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -178,3 +178,9 @@
}
}
}
+
+.issuable-row {
+ background-color: $white-light;
+ cursor: -webkit-grab;
+ cursor: grab;
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index da0caa30c26..f310cc72da0 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -467,7 +467,7 @@ ul.notes {
}
.add-diff-note {
- margin-top: -4px;
+ margin-top: -8px;
border-radius: 40px;
background: $white-light;
padding: 4px;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 47dfc22d533..0c7019dc64f 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -94,6 +94,10 @@
padding: 10px 8px;
}
+ td.stage-cell {
+ padding: 10px 0;
+ }
+
.commit-link {
padding: 9px 8px 10px;
}
@@ -183,51 +187,11 @@
}
}
- .stage-cell {
- font-size: 0;
- padding: 10px 4px;
-
- > .stage-container > div > button > span > svg,
- > .stage-container > button > svg {
- height: 22px;
- width: 22px;
- position: absolute;
- top: -1px;
- left: -1px;
- z-index: 2;
- overflow: visible;
- }
-
- .stage-container {
- display: inline-block;
- position: relative;
- margin-right: 6px;
-
- .tooltip {
- white-space: nowrap;
- }
-
- .tooltip-inner {
- padding: 3px 4px;
- }
-
- &:not(:last-child) {
- &::after {
- content: '';
- width: 7px;
- position: absolute;
- right: -7px;
- top: 10px;
- border-bottom: 2px solid $border-color;
- }
- }
- }
- }
-
.duration,
.finished-at {
color: $gl-text-color-secondary;
margin: 4px 0;
+ white-space: nowrap;
.fa {
font-size: 12px;
@@ -310,6 +274,50 @@
}
}
+.stage-cell {
+ font-size: 0;
+ padding: 10px 4px;
+
+ > .stage-container > div > button > span > svg,
+ > .stage-container > button > svg {
+ height: 22px;
+ width: 22px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ z-index: 2;
+ overflow: visible;
+ }
+
+ .stage-container {
+ display: inline-block;
+ position: relative;
+ height: 22px;
+ margin: 3px 6px 3px 0;
+
+ // Hack to show a button tooltip inline
+ button.has-tooltip + .tooltip {
+ min-width: 105px;
+ }
+
+ // Bootstrap way of showing the content inline for anchors.
+ a.has-tooltip {
+ white-space: nowrap;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ width: 7px;
+ position: absolute;
+ right: -7px;
+ top: 10px;
+ border-bottom: 2px solid $border-color;
+ }
+ }
+ }
+}
+
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
@@ -494,31 +502,27 @@
// Action Icons in big pipeline-graph nodes
> .ci-action-icon-container .ci-action-icon-wrapper {
- i {
- color: $border-color;
- border-radius: 100%;
- border: 1px solid $border-color;
- padding: 5px 6px;
- font-size: 13px;
- background: $white-light;
- height: 30px;
- width: 30px;
-
- &::before {
- position: relative;
- top: 3px;
- left: 3px;
- }
+ height: 30px;
+ width: 30px;
+ background: $white-light;
+ border: 1px solid $border-color;
+ border-radius: 100%;
+ display: block;
- &:hover {
- color: $gl-text-color;
- background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-bg;
- }
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $stage-hover-bg;
}
- .ci-play-icon {
- padding: 5px 5px 5px 7px;
+ svg {
+ fill: $border-color;
+ position: relative;
+ left: -1px;
+ top: -1px;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
}
}
@@ -657,7 +661,7 @@
font-weight: 100;
font-size: 15px;
position: absolute;
- right: 5px;
+ right: 13px;
top: 8px;
}
@@ -669,7 +673,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: 200;
+ font-weight: normal;
}
// Dropdown button in mini pipeline graph
@@ -825,11 +829,23 @@
&:hover,
&:focus {
- text-decoration: none;
- color: $gl-text-color;
background-color: $stage-hover-bg;
border: 1px solid transparent;
}
+
+ svg {
+ width: 22px;
+ height: 22px;
+ left: -6px;
+ position: relative;
+ top: -3px;
+ fill: $action-icon-color;
+ }
+
+ &:hover svg,
+ &:focus svg {
+ fill: $gl-text-color;
+ }
}
// link to the build
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 722b3006f7c..8031c4467a4 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -201,10 +201,6 @@
color: $note-disabled-comment-color;
}
-.datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
- text-align: center;
-}
-
.created-personal-access-token-container {
#created-personal-access-token {
width: 90%;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 01675acc62e..0d5604aae69 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -76,6 +76,10 @@
font-size: 14px;
}
+ .action-name {
+ font-weight: normal;
+ }
+
.todo-body {
.todo-note {
word-wrap: break-word;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 948921efc0b..8fafe472621 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -171,8 +171,6 @@
.tree-controls {
float: right;
margin-top: 11px;
- position: relative;
- z-index: 2;
.project-action-button {
margin-left: $btn-side-margin;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 543d5eac504..b0f5d4a9933 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:plantuml_url,
:max_artifacts_size,
:max_attachment_size,
+ :max_pages_size,
:metrics_enabled,
:metrics_host,
:metrics_method_call_threshold,
@@ -137,6 +138,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:user_default_external,
:user_oauth_applications,
:version_check_enabled,
+ :terminal_max_session_time,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index c491e5c7550..8360ce08bdc 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,7 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.limit(10)
+ @projects = Project.with_route.limit(10)
@users = User.limit(10)
- @groups = Group.limit(10)
+ @groups = Group.with_route.limit(10)
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index b7722a1d15d..cea3d088e94 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -2,7 +2,7 @@ class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index
- @groups = Group.with_statistics
+ @groups = Group.with_statistics.with_route
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
@@ -49,7 +49,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index b09ae423096..39c8c6d8a0c 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -45,7 +45,7 @@ class Admin::ProjectsController < Admin::ApplicationController
protected
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index bc65dcc33d3..70ac6a75434 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
private
def project
- @project = Project.find_with_namespace(
+ @project = Project.find_by_full_path(
[params[:namespace_id], '/', params[:project_id]].join('')
)
@project || render_404
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index aa0f8d434dc..1cd50852e89 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -175,7 +175,7 @@ class Admin::UsersController < Admin::ApplicationController
def user_params_ce
[
- :admin,
+ :access_level,
:avatar,
:bio,
:can_create_group,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bb47e2a8bf7..bf6be3d516b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_private_token!
before_action :authenticate_user!
before_action :validate_user_service_ticket!
- before_action :reject_blocked!
before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check
@@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
- def reject_blocked!
- if current_user && current_user.blocked?
- sign_out current_user
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- redirect_to new_user_session_path
- end
- end
-
def after_sign_in_path_for(resource)
- if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked?
- sign_out resource
- flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
- new_user_session_path
- else
- stored_location_for(:redirect) || stored_location_for(resource) || root_path
- end
+ stored_location_for(:redirect) || stored_location_for(resource) || root_path
end
def after_sign_out_path_for(resource)
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index 6f43ce5226d..6286d67d30c 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -4,13 +4,15 @@ module CreatesCommit
def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
set_commit_variables
+ start_branch = @mr_target_branch unless initial_commit?
commit_params = @commit_params.merge(
- source_project: @project,
- source_branch: @ref,
- target_branch: @target_branch
+ start_project: @mr_target_project,
+ start_branch: start_branch,
+ target_branch: @mr_source_branch
)
- result = service.new(@tree_edit_project, current_user, commit_params).execute
+ result = service.new(
+ @mr_source_project, current_user, commit_params).execute
if result[:status] == :success
update_flash_notice(success_notice)
@@ -89,20 +91,18 @@ module CreatesCommit
@mr_source_project != @mr_target_project
end
- def different_branch?
- @mr_source_branch != @mr_target_branch || different_project?
- end
-
def create_merge_request?
- params[:create_merge_request].present? && different_branch?
+ # XXX: Even if the field is set, if we're checking the same branch
+ # as the target branch in the same project,
+ # we don't want to create a merge request.
+ params[:create_merge_request].present? &&
+ (different_project? || @ref != @target_branch)
end
+ # TODO: We should really clean this up
def set_commit_variables
- @mr_source_branch ||= @target_branch
-
if can?(current_user, :push_code, @project)
# Edit file in this project
- @tree_edit_project = @project
@mr_source_project = @project
if @project.forked?
@@ -112,15 +112,34 @@ module CreatesCommit
else
# Merge request to this project
@mr_target_project = @project
- @mr_target_branch ||= @ref
+ @mr_target_branch = @ref || @target_branch
end
else
- # Edit file in fork
- @tree_edit_project = current_user.fork_of(@project)
# Merge request from fork to this project
- @mr_source_project = @tree_edit_project
+ @mr_source_project = current_user.fork_of(@project)
@mr_target_project = @project
- @mr_target_branch ||= @ref
+ @mr_target_branch = @ref || @target_branch
end
+
+ @mr_source_branch = guess_mr_source_branch
+ end
+
+ def initial_commit?
+ @mr_target_branch.nil? ||
+ !@mr_target_project.repository.branch_exists?(@mr_target_branch)
+ end
+
+ def guess_mr_source_branch
+ # XXX: Happens when viewing a commit without a branch. In this case,
+ # @target_branch would be the default branch for @mr_source_project,
+ # however we want a generated new branch here. Thus we can't use
+ # @target_branch, but should pass nil to indicate that we want a new
+ # branch instead of @target_branch.
+ return if
+ create_merge_request? &&
+ # XXX: Don't understand why rubocop prefers this indention
+ @mr_source_project.repository.branch_exists?(@target_branch)
+
+ @target_branch
end
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 6247934f81e..a6e158ebae6 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -9,6 +9,28 @@ module IssuableCollections
private
+ def issuable_meta_data(issuable_collection)
+ # map has to be used here since using pluck or select will
+ # throw an error when ordering issuables by priority which inserts
+ # a new order into the collection.
+ # We cannot use reorder to not mess up the paginated collection.
+ issuable_ids = issuable_collection.map(&:id)
+ issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
+ issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
+
+ issuable_ids.each_with_object({}) do |id, issuable_meta|
+ downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
+ upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
+ notes = issuable_note_count.find { |notes| notes.noteable_id == id }
+
+ issuable_meta[id] = Issuable::IssuableMeta.new(
+ upvotes.try(:count).to_i,
+ downvotes.try(:count).to_i,
+ notes.try(:count).to_i
+ )
+ end
+ end
+
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index b46adcceb60..fb5edb34370 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -9,6 +9,9 @@ module IssuesAction
.non_archived
.page(params[:page])
+ @collection_type = "Issue"
+ @issuable_meta_data = issuable_meta_data(@issues)
+
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index fdb05bb3228..6229759dcf1 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.page(params[:page])
+
+ @collection_type = "MergeRequest"
+ @issuable_meta_data = issuable_meta_data(@merge_requests)
end
private
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 99acd98ae13..a6891149bfa 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -1,13 +1,15 @@
module SpammableActions
extend ActiveSupport::Concern
+ include Recaptcha::Verify
+
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
end
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
- redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
+ redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
@@ -15,6 +17,15 @@ module SpammableActions
private
+ def recaptcha_params
+ return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
+
+ {
+ recaptcha_verified: true,
+ spam_log_id: params[:spam_log_id]
+ }
+ end
+
def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable!
access_denied! unless current_user.admin?
end
+
+ def render_recaptcha?
+ return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
+ return false unless Gitlab::Recaptcha.enabled?
+
+ spammable.spam
+ end
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index de6bc689bb7..0b7cf8167f0 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,5 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @group_members = current_user.group_members.includes(:source).page(params[:page])
+ @group_members = current_user.group_members.includes(source: :route).page(params[:page])
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index c08eb811532..3ba8c2f8bb9 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
- @last_push = current_user.recent_push
-
respond_to do |format|
- format.html
+ format.html { @last_push = current_user.recent_push }
format.atom do
event_filter
load_events
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4dda4e51f6a..79d420a32d3 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -4,6 +4,7 @@ class DashboardController < Dashboard::ApplicationController
before_action :event_filter, only: :activity
before_action :projects, only: [:issues, :merge_requests]
+ before_action :set_show_full_reference, only: [:issues, :merge_requests]
respond_to :html
@@ -34,4 +35,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def set_show_full_reference
+ @show_full_reference = true
+ end
end
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
index a1ab8b99048..baf54520b9c 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,5 +1,5 @@
class Explore::ApplicationController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'explore'
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 4f273a8d4f0..0cbf3eb58a3 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -9,7 +9,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
- @members = @group.group_members
+ @members = GroupMembersFinder.new(@group).execute
@members = @members.non_invite unless can?(current_user, :admin_group, @group)
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f81237db991..7ed54479599 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -13,9 +13,11 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :group_projects, only: [:show, :projects, :activity, :issues, :merge_requests]
+ before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :event_filter, only: [:activity]
+ before_action :user_actions, only: [:show, :subgroups]
+
layout :determine_layout
def index
@@ -37,13 +39,6 @@ class GroupsController < Groups::ApplicationController
end
def show
- if current_user
- @last_push = current_user.recent_push
- @notification_setting = current_user.notification_settings_for(group)
- end
-
- @nested_groups = group.children
-
setup_projects
respond_to do |format|
@@ -62,6 +57,11 @@ class GroupsController < Groups::ApplicationController
end
end
+ def subgroups
+ @nested_groups = group.children
+ @nested_groups = @nested_groups.search(params[:filter_groups]) if params[:filter_groups].present?
+ end
+
def activity
respond_to do |format|
format.html
@@ -84,14 +84,14 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
- @group.reset_path!
+ @group.restore_path!
render action: "edit"
end
end
def destroy
- DestroyGroupService.new(@group, current_user).async_execute
+ Groups::DestroyService.new(@group, current_user).async_execute
redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
@@ -99,13 +99,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
+ options = {}
+ options[:only_owned] = true if params[:shared] == '0'
+ options[:only_shared] = true if params[:shared] == '1'
+
+ @projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:filter_projects].blank?
-
- @shared_projects = GroupProjectsFinder.new(group, only_shared: true).execute(current_user)
end
def authorize_create_group!
@@ -138,7 +141,8 @@ class GroupsController < Groups::ApplicationController
:public,
:request_access_enabled,
:share_with_group_lock,
- :visibility_level
+ :visibility_level,
+ :parent_id
]
end
@@ -147,4 +151,11 @@ class GroupsController < Groups::ApplicationController
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def user_actions
+ if current_user
+ @last_push = current_user.recent_push
+ @notification_setting = current_user.notification_settings_for(group)
+ end
+ end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 37feff79999..87c0f8905ff 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,5 +1,5 @@
class HelpController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
layout 'help'
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
index f3759b4c0ea..6b1e64ce819 100644
--- a/app/controllers/koding_controller.rb
+++ b/app/controllers/koding_controller.rb
@@ -1,5 +1,5 @@
class KodingController < ApplicationController
- before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ before_action :check_integration!
layout 'koding'
def index
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index b2ff36f6538..db33b60b229 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -18,13 +18,13 @@ class Projects::ApplicationController < ApplicationController
# to
# localhost/group/project
#
- if id =~ /\.git\Z/
+ if params[:format] == 'git'
redirect_to request.original_url.gsub(/\.git\/?\Z/, '')
return
end
project_path = "#{namespace}/#{id}"
- @project = Project.find_with_namespace(project_path)
+ @project = Project.find_by_full_path(project_path)
if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 9940263ae24..a1db856dcfb 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end
def edit
@@ -59,10 +61,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
- success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
- failure_view: :show,
- failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+ create_commit(Files::DestroyService, success_notice: "The file has been successfully deleted.",
+ success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index dc33e1405f2..61fef4dc133 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -7,7 +7,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
- issues = issues.page(params[:page])
+ issues = issues.page(params[:page]).per(params[:per] || 20)
render json: {
issues: serialize_as_json(issues),
@@ -59,7 +59,7 @@ module Projects
end
def filter_params
- params.merge(board_id: params[:board_id], id: params[:list_id])
+ params.merge(board_id: params[:board_id], id: params[:list_id]).compact
end
def move_params
@@ -73,7 +73,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:iid, :title, :confidential, :due_date],
+ only: [:id, :iid, :title, :confidential, :due_date],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index bfc59bcc862..e10d7992db7 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -30,6 +30,16 @@ class Projects::CommitController < Projects::ApplicationController
end
def pipelines
+ @pipelines = @commit.pipelines.order(id: :desc)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
+ end
end
def branches
@@ -39,7 +49,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def revert
- assign_change_commit_vars(@commit.revert_branch_name)
+ assign_change_commit_vars
return render_404 if @target_branch.blank?
@@ -48,7 +58,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def cherry_pick
- assign_change_commit_vars(@commit.cherry_pick_branch_name)
+ assign_change_commit_vars
return render_404 if @target_branch.blank?
@@ -84,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
+
+ @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end
def define_note_vars
@@ -105,11 +117,9 @@ class Projects::CommitController < Projects::ApplicationController
}
end
- def assign_change_commit_vars(mr_source_branch)
+ def assign_change_commit_vars
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
- @mr_source_branch = mr_source_branch
- @mr_target_branch = @target_branch
@commit_params = {
commit: @commit,
create_merge_request: params[:create_merge_request].present? || different_project?
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d32966645c8..c6651254d70 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -46,7 +46,8 @@ class Projects::CompareController < Projects::ApplicationController
end
def define_diff_vars
- @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
+ @compare = CompareService.new(@project, @head_ref)
+ .execute(@project, @start_ref)
if @compare
@commits = @compare.commits
@@ -56,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options)
+ environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@diff_notes_disabled = true
@grouped_diff_discussions = {}
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 87cc36253f1..0ec8f5bd64a 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index
@scope = params[:scope]
- @environments = project.environments
+ @environments = project.environments.includes(:last_deployment)
respond_to do |format|
format.html
@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
- return render_404 unless @environment.stoppable?
+ return render_404 unless @environment.available?
- new_action = @environment.stop!(current_user)
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
+ stop_action = @environment.stop_with_action!(current_user)
+
+ if stop_action
+ redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ else
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ end
end
def terminal
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 70845617d3c..216c158e41e 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -79,7 +79,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
if project_id.blank?
@project = nil
else
- @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
+ @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}")
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8472ceca329..744a4af1c51 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
- @issues = issues_collection
- @issues = @issues.page(params[:page])
+ @collection_type = "Issue"
+ @issues = issues_collection
+ @issues = @issues.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@issues)
+
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
end
@@ -93,15 +96,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create
extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
+ extra_params.merge!(recaptcha_params)
+
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
- if @issue.valid?
- redirect_to issue_path(@issue)
- else
- render :new
- end
+ html_response_create
end
format.js do
@link = @issue.attachment.url.to_js
@@ -178,6 +179,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected
+ def html_response_create
+ if @issue.valid?
+ redirect_to issue_path(@issue)
+ elsif render_recaptcha?
+ if params[:recaptcha_verification]
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
+ end
+
+ render :verify
+ else
+ render :new
+ end
+ end
+
def issue
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 824ed7be73e..1593b5c1afb 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -2,12 +2,13 @@ class Projects::LabelsController < Projects::ApplicationController
include ToggleSubscriptionAction
before_action :module_enabled
- before_action :label, only: [:edit, :update, :destroy]
+ before_action :label, only: [:edit, :update, :destroy, :promote]
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,
:set_priorities]
+ before_action :authorize_admin_group!, only: [:promote]
respond_to :js, :html
@@ -71,13 +72,7 @@ class Projects::LabelsController < Projects::ApplicationController
@label.destroy
@labels = find_labels
- respond_to do |format|
- format.html do
- redirect_to(namespace_project_labels_path(@project.namespace, @project),
- notice: 'Label was removed')
- end
- format.js
- end
+ redirect_to(namespace_project_labels_path(@project.namespace, @project), notice: 'Label was removed')
end
def remove_priority
@@ -108,6 +103,32 @@ class Projects::LabelsController < Projects::ApplicationController
end
end
+ def promote
+ promote_service = Labels::PromoteService.new(@project, @current_user)
+
+ begin
+ return render_404 unless promote_service.execute(@label)
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Label was promoted to a Group Label')
+ end
+ format.js
+ end
+ rescue ActiveRecord::RecordInvalid => e
+ Gitlab::AppLogger.error "Failed to promote label \"#{@label.title}\" to group label"
+ Gitlab::AppLogger.error e
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_labels_path(@project.namespace, @project),
+ notice: 'Failed to promote label due to internal error. Please contact administrators.')
+ end
+ format.js
+ end
+ end
+ end
+
protected
def module_enabled
@@ -135,4 +156,8 @@ class Projects::LabelsController < Projects::ApplicationController
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project)
end
+
+ def authorize_admin_group!
+ return render_404 unless can?(current_user, :admin_group, @project.group)
+ end
end
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 440259b643c..8a5a645ed0e 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
+
+ if Guest.can?(:download_code, project)
+ object[:authenticated] = true
+ end
else
object[:error] = {
code: 404,
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index 01d99c7df35..38f7e6eb5e9 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -34,7 +34,7 @@ class Projects::MattermostsController < Projects::ApplicationController
end
def teams
- @teams ||= @service.list_teams(current_user)
+ @teams, @teams_error_message = @service.list_teams(current_user)
end
def service
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9ac5bf4b9f8..c3e1760f168 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
- @merge_requests = merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page])
+ @collection_type = "MergeRequest"
+ @merge_requests = merge_requests_collection
+ @merge_requests = @merge_requests.page(params[:page])
+ @issuable_meta_data = issuable_meta_data(@merge_requests)
+
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end
@@ -103,6 +106,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ @environment = @merge_request.environments_for(current_user).last
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
@@ -214,12 +219,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render 'show'
end
- format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+
+ format.json do
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
end
end
def new
- define_new_vars
+ respond_to do |format|
+ format.html { define_new_vars }
+ format.json do
+ define_pipelines_vars
+
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent(@pipelines)
+ end
+ end
end
def new_diffs
@@ -236,7 +255,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@diff_notes_disabled = true
- render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ @environment = @merge_request.environments_for(current_user).last
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) }
end
end
end
@@ -425,7 +446,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
- coverage: coverage
+ coverage: coverage,
+ pipeline: pipeline.try(:id)
}
render json: response
@@ -434,14 +456,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status
environments =
begin
- @merge_request.environments.map do |environment|
- next unless can?(current_user, :read_environment, environment)
-
+ @merge_request.environments_for(current_user).map do |environment|
project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
- if environment.stoppable? && can?(current_user, :create_deployment, environment)
+ if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index c5d93ce25bc..b033f7b5ea9 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -51,7 +51,7 @@ class Projects::NotesController < Projects::ApplicationController
def destroy
if note.editable?
- Notes::DeleteService.new(project, current_user).execute(note)
+ Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
new file mode 100644
index 00000000000..fbd18b68141
--- /dev/null
+++ b/app/controllers/projects/pages_controller.rb
@@ -0,0 +1,22 @@
+class Projects::PagesController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_read_pages!, only: [:show]
+ before_action :authorize_update_pages!, except: [:show]
+
+ def show
+ @domains = @project.pages_domains.order(:domain)
+ end
+
+ def destroy
+ project.remove_pages
+ project.pages_domains.destroy_all
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Pages were removed')
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
new file mode 100644
index 00000000000..b8c253f6ae3
--- /dev/null
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -0,0 +1,49 @@
+class Projects::PagesDomainsController < Projects::ApplicationController
+ layout 'project_settings'
+
+ before_action :authorize_update_pages!, except: [:show]
+ before_action :domain, only: [:show, :destroy]
+
+ def show
+ end
+
+ def new
+ @domain = @project.pages_domains.new
+ end
+
+ def create
+ @domain = @project.pages_domains.create(pages_domain_params)
+
+ if @domain.valid?
+ redirect_to namespace_project_pages_path(@project.namespace, @project)
+ else
+ render 'new'
+ end
+ end
+
+ def destroy
+ @domain.destroy
+
+ respond_to do |format|
+ format.html do
+ redirect_to(namespace_project_pages_path(@project.namespace, @project),
+ notice: 'Domain was removed')
+ end
+ format.js
+ end
+ end
+
+ private
+
+ def pages_domain_params
+ params.require(:pages_domain).permit(
+ :certificate,
+ :key,
+ :domain
+ )
+ end
+
+ def domain
+ @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s)
+ end
+end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 53ce23221ed..c8c80551ac9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -2,20 +2,13 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
before_action :authorize_admin_pipeline!
def show
- @ref = params[:ref] || @project.default_branch || 'master'
-
- @badges = [Gitlab::Badge::Build::Status,
- Gitlab::Badge::Coverage::Report]
-
- @badges.map! do |badge|
- badge.new(@project, @ref).metadata
- end
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project, params: params)
end
def update
if @project.update_attributes(update_params)
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
- redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
else
render 'show'
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 9a438d5512c..2f422d352ed 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -68,8 +68,12 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def access_levels_options
{
- push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
- merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ push_access_levels: {
+ "Roles" => ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
+ },
+ merge_access_levels: {
+ "Roles" => ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ }
}
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 53c36635efe..74c54037ba9 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,11 +5,7 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @project_runners = project.runners.ordered
- @assignable_runners = current_user.ci_authorized_runners.
- assignable_for(project).ordered.page(params[:page]).per(20)
- @shared_runners = Ci::Runner.shared.active
- @shared_runners_count = @shared_runners.count(:all)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
@@ -53,7 +49,7 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners
project.toggle!(:shared_runners_enabled)
- redirect_to namespace_project_runners_path(project.namespace, project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
protected
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
new file mode 100644
index 00000000000..6f009d61950
--- /dev/null
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -0,0 +1,44 @@
+module Projects
+ module Settings
+ class CiCdController < Projects::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ define_runners_variables
+ define_secret_variables
+ define_triggers_variables
+ define_badges_variables
+ end
+
+ private
+
+ def define_runners_variables
+ @project_runners = @project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
+ @shared_runners = Ci::Runner.shared.active
+ @shared_runners_count = @shared_runners.count(:all)
+ end
+
+ def define_secret_variables
+ @variable = Ci::Variable.new
+ end
+
+ def define_triggers_variables
+ @triggers = @project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def define_badges_variables
+ @ref = params[:ref] || @project.default_branch || 'master'
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 02a97c1c574..5d193f26a8e 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,8 +1,9 @@
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- @snippet = CreateSnippetService.new(@project, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(@project, current_user, create_params).execute
if @snippet.valid?
respond_with(@snippet,
@@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 92359745cec..b2c11ea4156 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -4,8 +4,7 @@ class Projects::TriggersController < Projects::ApplicationController
layout 'project_settings'
def index
- @triggers = project.triggers
- @trigger = Ci::Trigger.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def create
@@ -13,17 +12,18 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger.save
if @trigger.valid?
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
else
@triggers = project.triggers.select(&:persisted?)
- render :index
+ render action: "show"
end
end
def destroy
trigger.destroy
+ flash[:alert] = "Trigger removed"
- redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index e617be8f9fb..61686499bd3 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,6 +1,6 @@
class Projects::UploadsController < Projects::ApplicationController
- skip_before_action :reject_blocked!, :project,
- :repository, if: -> { action_name == 'show' && image_or_video? }
+ skip_before_action :project, :repository,
+ if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
@@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController
namespace = params[:namespace_id]
id = params[:project_id]
- file_project = Project.find_with_namespace("#{namespace}/#{id}")
+ file_project = Project.find_by_full_path("#{namespace}/#{id}")
if file_project.nil?
@uploader = nil
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index 6f068729390..a4d1b1ee69b 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -4,7 +4,7 @@ class Projects::VariablesController < Projects::ApplicationController
layout 'project_settings'
def index
- @variable = Ci::Variable.new
+ redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def show
@@ -25,9 +25,10 @@ class Projects::VariablesController < Projects::ApplicationController
@variable = Ci::Variable.new(project_params)
if @variable.valid? && @project.variables << @variable
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ flash[:notice] = 'Variables were successfully updated.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project)
else
- render action: "index"
+ render "show"
end
end
@@ -35,7 +36,7 @@ class Projects::VariablesController < Projects::ApplicationController
@key = @project.variables.find(params[:id])
@key.destroy
- redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variable was successfully removed.'
+ redirect_to namespace_project_settings_ci_cd_path(project.namespace, project), notice: 'Variable was successfully removed.'
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 116c854b1ae..0faa71c4d7d 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -84,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
- @page.delete if @page
+ @page&.delete
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home),
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 444ff837bb3..acca821782c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -231,12 +231,16 @@ class ProjectsController < Projects::ApplicationController
end
def refs
+ branches = BranchesFinder.new(@repository, params).execute.map(&:name)
+
options = {
- 'Branches' => @repository.branch_names,
+ 'Branches' => branches.take(100),
}
unless @repository.tag_count.zero?
- options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ tags = TagsFinder.new(@repository, params).execute.map(&:name)
+
+ options['Tags'] = tags.take(100)
end
# If reference is commit id - we should add it to branch/tag selectbox
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index bf27f3d4d51..b44f38d4a0c 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -17,20 +17,20 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
- flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
+ flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
render action: 'new'
end
end
def destroy
- DeleteUserService.new(current_user).execute(current_user)
+ Users::DestroyService.new(current_user).execute(current_user)
respond_to do |format|
format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
- end
+ end
end
end
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 627be74a38f..db2817fadf6 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -7,6 +7,7 @@
# For users who haven't customized the setting, we simply delegate to
# `DashboardController#show`, which is the default.
class RootController < Dashboard::ProjectsController
+ skip_before_action :authenticate_user!, only: [:index]
before_action :redirect_to_custom_dashboard, only: [:index]
def index
@@ -16,7 +17,7 @@ class RootController < Dashboard::ProjectsController
private
def redirect_to_custom_dashboard
- return unless current_user
+ return redirect_to new_user_session_path unless current_user
case current_user.dashboard
when 'stars'
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 6576ebd5235..612d69cf557 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked!
+ skip_before_action :authenticate_user!
include SearchHelper
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index dee57e4a388..b169d993688 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,5 +1,6 @@
class SnippetsController < ApplicationController
include ToggleAwardEmoji
+ include SpammableActions
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
@@ -40,8 +41,8 @@ class SnippetsController < ApplicationController
end
def create
- @snippet = CreateSnippetService.new(nil, current_user,
- snippet_params).execute
+ create_params = snippet_params.merge(request: request)
+ @snippet = CreateSnippetService.new(nil, current_user, create_params).execute
respond_with @snippet.becomes(Snippet)
end
@@ -96,6 +97,7 @@ class SnippetsController < ApplicationController
end
end
alias_method :awardable, :snippet
+ alias_method :spammable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb
new file mode 100644
index 00000000000..a59f8c1efa3
--- /dev/null
+++ b/app/finders/environments_finder.rb
@@ -0,0 +1,55 @@
+class EnvironmentsFinder
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user, params = {})
+ @project, @current_user, @params = project, current_user, params
+ end
+
+ def execute
+ deployments = project.deployments
+ deployments =
+ if ref
+ deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref'
+ deployments.where(deployments_query, ref: ref.to_s)
+ elsif commit
+ deployments.where(sha: commit.sha)
+ else
+ deployments.none
+ end
+
+ environment_ids = deployments
+ .group(:environment_id)
+ .select(:environment_id)
+
+ environments = project.environments.available
+ .where(id: environment_ids).order_by_last_deployed_at.to_a
+
+ environments.select! do |environment|
+ Ability.allowed?(current_user, :read_environment, environment)
+ end
+
+ if ref && commit
+ environments.select! do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
+ if ref && params[:recently_updated]
+ environments.select! do |environment|
+ environment.recently_updated_on_branch?(ref)
+ end
+ end
+
+ environments
+ end
+
+ private
+
+ def ref
+ params[:ref].try(:to_s)
+ end
+
+ def commit
+ params[:commit]
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
new file mode 100644
index 00000000000..9f2206346ce
--- /dev/null
+++ b/app/finders/group_members_finder.rb
@@ -0,0 +1,20 @@
+class GroupMembersFinder < Projects::ApplicationController
+ def initialize(group)
+ @group = group
+ end
+
+ def execute
+ group_members = @group.members
+
+ return group_members unless @group.parent
+
+ parents_members = GroupMember.non_request.
+ where(source_id: @group.ancestors.select(:id)).
+ where.not(user_id: @group.users.select(:id))
+
+ wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+
+ GroupMember.where(wheres.join(' OR '))
+ end
+end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index aa8f4c1d0e4..3b9a421b118 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder
projects = []
if current_user
- if @group.users.include?(current_user) || current_user.admin?
+ if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
else
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 4e43f42e9e1..d932a17883f 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -2,7 +2,7 @@ class GroupsFinder < UnionFinder
def execute(current_user = nil)
segments = all_groups(current_user)
- find_union(segments, Group).order_id_desc
+ find_union(segments, Group).with_route.order_id_desc
end
private
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index c7911736812..18ec45f300d 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -3,7 +3,7 @@ class ProjectsFinder < UnionFinder
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
- find_union(segments, Project)
+ find_union(segments, Project).with_route
end
private
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a112928c6de..bee323993a0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -37,7 +37,7 @@ module ApplicationHelper
if project_id.is_a?(Project)
project_id
else
- Project.find_with_namespace(project_id)
+ Project.find_by_full_path(project_id)
end
if project.avatar_url
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index c3508443d8a..311a70725ab 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -21,7 +21,7 @@ module BlobHelper
options[:link_opts])
if !on_top_of_branch?(project, ref)
- button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
@@ -32,7 +32,7 @@ module BlobHelper
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
- link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post
+ link_to "Edit", fork_path, class: 'btn', method: :post
end
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 38c586ccd31..f43827da446 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -6,7 +6,9 @@ module BoardsHelper
endpoint: namespace_project_boards_path(@project.namespace, @project),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, @project)}",
- issue_link_base: namespace_project_issues_path(@project.namespace, @project)
+ issue_link_base: namespace_project_issues_path(@project.namespace, @project),
+ root_path: root_path,
+ bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project),
}
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index e9461b9f859..8aad39e148b 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -194,17 +194,28 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff_new_path, project)
+ def view_file_button(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)),
- class: 'btn view-file js-view-file btn-file-option'
+ class: 'btn view-file js-view-file'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id')
end
end
+ def view_on_environment_button(commit_sha, diff_new_path, environment)
+ return unless environment && commit_sha
+
+ external_url = environment.external_url_for(diff_new_path, commit_sha)
+ return unless external_url
+
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ icon('external-link')
+ end
+ end
+
def truncate_sha(sha)
Commit.truncate_sha(sha)
end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 2159e4ce21a..f16a63e2178 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -211,8 +211,12 @@ module GitlabRoutingHelper
def project_settings_integrations_path(project, *args)
namespace_project_settings_integrations_path(project.namespace, project, *args)
end
-
+
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
+
+ def project_settings_ci_cd_path(project, *args)
+ namespace_project_settings_ci_cd_path(project.namespace, project, *args)
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index e5bb8b93e76..03354c235eb 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -162,6 +162,10 @@ module IssuablesHelper
]
end
+ def issuable_reference(issuable)
+ @show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
+ end
+
def issuable_filter_present?
issuable_filter_params.any? { |k| params.key?(k) }
end
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 0e456214d37..320dd89c9d3 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -1,5 +1,8 @@
module JavascriptHelper
def page_specific_javascript_tag(js)
- javascript_include_tag asset_path(js), { "data-turbolinks-track" => true }
+ javascript_include_tag asset_path(js)
+ end
+ def page_specific_javascript_bundle_tag(js)
+ javascript_include_tag(*webpack_asset_paths(js))
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8c2c4e8833b..b5f8c23a667 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -20,8 +20,8 @@ module MergeRequestsHelper
end
def mr_widget_refresh_url(mr)
- if mr && mr.source_project
- merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
+ if mr && mr.target_project
+ merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else
''
end
@@ -64,11 +64,11 @@ module MergeRequestsHelper
end
def mr_closes_issues
- @mr_closes_issues ||= @merge_request.closes_issues
+ @mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
def mr_issues_mentioned_but_not_closing
- @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+ @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
def mr_change_branches_path(merge_request)
@@ -143,4 +143,16 @@ module MergeRequestsHelper
def different_base?(version1, version2)
version1 && version2 && version1.base_commit_sha != version2.base_commit_sha
end
+
+ def merge_params(merge_request)
+ {
+ merge_when_build_succeeds: true,
+ should_remove_source_branch: true,
+ sha: merge_request.diff_head_sha
+ }.merge(merge_params_ee(merge_request))
+ end
+
+ def merge_params_ee(merge_request)
+ {}
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 6654f6997ce..8ff8db16514 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -56,7 +56,7 @@ module SearchHelper
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
- { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
]
end
@@ -89,7 +89,7 @@ module SearchHelper
{
category: "Groups",
id: group.id,
- label: "#{search_result_sanitize(group.name)}",
+ label: "#{search_result_sanitize(group.full_name)}",
url: group_path(group)
}
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index c568cca9e5e..845f1a0e840 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -15,6 +15,7 @@ module TodosHelper
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
+ when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
end
end
@@ -86,7 +87,10 @@ module TodosHelper
[
{ id: '', text: 'Any Action' },
{ id: Todo::ASSIGNED, text: 'Assigned' },
- { id: Todo::MENTIONED, text: 'Mentioned' }
+ { id: Todo::MENTIONED, text: 'Mentioned' },
+ { id: Todo::MARKED, text: 'Added' },
+ { id: Todo::BUILD_FAILED, text: 'Pipelines' },
+ { id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
]
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 3a83ae15dd8..fc93acfe63e 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -93,10 +93,6 @@ module VisibilityLevelHelper
current_application_settings.default_project_visibility
end
- def default_snippet_visibility
- current_application_settings.default_snippet_visibility
- end
-
def default_group_visibility
current_application_settings.default_group_visibility
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0cd3456b4de..5b9226a6b81 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -151,7 +151,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
- headers[:subject].prepend('Re: ') if headers[:subject]
+ headers[:subject]&.prepend('Re: ')
mail_thread(model, headers)
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 2df8b071e13..74b358d8c40 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -111,32 +111,30 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period }
+ validates :terminal_max_session_time,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
- unless value.nil?
- value.each do |level|
- unless Gitlab::VisibilityLevel.options.has_value?(level)
- record.errors.add(attr, "'#{level}' is not a valid visibility level")
- end
+ value&.each do |level|
+ unless Gitlab::VisibilityLevel.options.has_value?(level)
+ record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end
end
validates_each :import_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Gitlab::ImportSources.options.has_value?(source)
- record.errors.add(attr, "'#{source}' is not a import source")
- end
+ value&.each do |source|
+ unless Gitlab::ImportSources.options.has_value?(source)
+ record.errors.add(attr, "'#{source}' is not a import source")
end
end
end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
- unless value.nil?
- value.each do |source|
- unless Devise.omniauth_providers.include?(source.to_sym)
- record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
- end
+ value&.each do |source|
+ unless Devise.omniauth_providers.include?(source.to_sym)
+ record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end
end
@@ -204,7 +202,8 @@ class ApplicationSetting < ActiveRecord::Base
signin_enabled: Settings.gitlab['signin_enabled'],
signup_enabled: Settings.gitlab['signup_enabled'],
two_factor_grace_period: 48,
- user_default_external: false
+ user_default_external: false,
+ terminal_max_session_time: 0
}
end
@@ -225,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base
end
def domain_whitelist_raw
- self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
+ self.domain_whitelist&.join("\n")
end
def domain_blacklist_raw
- self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
+ self.domain_blacklist&.join("\n")
end
def domain_whitelist_raw=(values)
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 46b17479d6d..6937ad3bdd9 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ class << self
+ def votes_for_collection(ids, type)
+ select('name', 'awardable_id', 'COUNT(*) as count').
+ where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
+ group('name', 'awardable_id')
+ end
+ end
+
def downvote?
self.name == DOWNVOTE_NAME
end
diff --git a/app/models/board.rb b/app/models/board.rb
index c56422914a9..2780acc67c0 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -5,10 +5,6 @@ class Board < ActiveRecord::Base
validates :project, presence: true
- def backlog_list
- lists.merge(List.backlog).take
- end
-
def done_list
lists.merge(List.done).take
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5fe8ddf69d7..8c1b076c2d7 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,7 @@ module Ci
belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable
+ has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment'
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
@@ -19,7 +20,7 @@ module Ci
end
serialize :options
- serialize :yaml_variables, Gitlab::Serialize::Ci::Variables
+ serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
@@ -41,7 +42,7 @@ module Ci
before_save :update_artifacts_size, if: :artifacts_file_changed?
before_save :ensure_token
- before_destroy { project }
+ before_destroy { unscoped_project }
after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
@@ -183,10 +184,6 @@ module Ci
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
@@ -256,7 +253,7 @@ module Ci
end
def project_id
- pipeline.project_id
+ gl_project_id
end
def project_name
@@ -275,29 +272,23 @@ module Ci
end
def update_coverage
- return unless project
- coverage_regex = project.build_coverage_regex
- return unless coverage_regex
coverage = extract_coverage(trace, coverage_regex)
-
- if coverage.is_a? Numeric
- update_attributes(coverage: coverage)
- end
+ update_attributes(coverage: coverage) if coverage.present?
end
def extract_coverage(text, regex)
- begin
- matches = text.scan(Regexp.new(regex)).last
- matches = matches.last if matches.kind_of?(Array)
- coverage = matches.gsub(/\d+(\.\d+)?/).first
+ return unless regex
- if coverage.present?
- coverage.to_f
- end
- rescue
- # if bad regex or something goes wrong we dont want to interrupt transition
- # so we just silentrly ignore error for now
+ matches = text.scan(Regexp.new(regex)).last
+ matches = matches.last if matches.kind_of?(Array)
+ coverage = matches.gsub(/\d+(\.\d+)?/).first
+
+ if coverage.present?
+ coverage.to_f
end
+ rescue
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
end
def has_trace_file?
@@ -422,16 +413,23 @@ module Ci
# This method returns old path to artifacts only if it already exists.
#
def artifacts_path
+ # We need the project even if it's soft deleted, because whenever
+ # we're really deleting the project, we'll also delete the builds,
+ # and in order to delete the builds, we need to know where to find
+ # the artifacts, which is depending on the data of the project.
+ # We need to retain the project in this case.
+ the_project = project || unscoped_project
+
old = File.join(created_at.utc.strftime('%Y_%m'),
- project.ci_id.to_s,
+ the_project.ci_id.to_s,
id.to_s)
old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if project.ci_id && File.directory?(old_store)
+ return old if the_project.ci_id && File.directory?(old_store)
File.join(
created_at.utc.strftime('%Y_%m'),
- project.id.to_s,
+ the_project.id.to_s,
id.to_s
)
end
@@ -457,6 +455,7 @@ module Ci
build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
+ PagesService.new(build_data).execute
project.running_or_pending_build_count(force: true)
end
@@ -522,6 +521,10 @@ module Ci
self.update(artifacts_expire_at: nil)
end
+ def coverage_regex
+ super || project.try(:build_coverage_regex)
+ end
+
def when
read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
end
@@ -561,6 +564,10 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
+ def unscoped_project
+ @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ end
+
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
@@ -599,6 +606,8 @@ module Ci
end
def update_project_statistics
+ return unless project
+
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fab8497ec7d..bbc358adb83 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -283,13 +283,7 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file ||= begin
- blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
- blob.load_all_data!(project.repository)
- blob.data
- rescue
- nil
- end
+ @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
end
def has_yaml_errors?
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 316bd2e512b..46f06733da1 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -100,8 +100,8 @@ class Commit
commit_reference(from_project, id, full: full)
end
- def reference_link_text(from_project = nil)
- commit_reference(from_project, short_id)
+ def reference_link_text(from_project = nil, full: false)
+ commit_reference(from_project, short_id, full: full)
end
def diff_line_count
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3517969eabc..5f53c48fc88 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -15,6 +15,11 @@ module Issuable
include Taskable
include TimeTrackable
+ # This object is used to gather issuable meta data for displaying
+ # upvotes, downvotes and notes count for issues and merge requests
+ # lists avoiding n+1 queries and improving performance.
+ IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
+
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -95,8 +100,8 @@ module Issuable
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
- previous_assignee.update_cache_counts if previous_assignee
- assignee.update_cache_counts if assignee
+ previous_assignee&.update_cache_counts
+ assignee&.update_cache_counts
end
# We want to use optimistic lock for cases when only title or description are involved
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index ef2c1e5d414..7e56e371b27 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -44,8 +44,15 @@ module Mentionable
end
def all_references(current_user = nil, extractor: nil)
- extractor ||= Gitlab::ReferenceExtractor.
- new(project, current_user)
+ # Use custom extractor if it's passed in the function parameters.
+ if extractor
+ @extractor = extractor
+ else
+ @extractor ||= Gitlab::ReferenceExtractor.
+ new(project, current_user)
+
+ @extractor.reset_memoized_values
+ end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
@@ -55,16 +62,20 @@ module Mentionable
skip_project_check: skip_project_check?
)
- extractor.analyze(text, options)
+ @extractor.analyze(text, options)
end
- extractor
+ @extractor
end
def mentioned_users(current_user = nil)
all_references(current_user).users
end
+ def directly_addressed_users(current_user = nil)
+ all_references(current_user).directly_addressed_users
+ end
+
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author)
refs = all_references(current_user)
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index e9450dd0c26..f449229864d 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -73,7 +73,7 @@ module Milestoneish
def memoize_per_user(user, method_name)
@memoized ||= {}
@memoized[method_name] ||= {}
- @memoized[method_name][user.try!(:id)] ||= yield
+ @memoized[method_name][user&.id] ||= yield
end
# override in a class that includes this module to get a faster query
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 2b93aa30c0f..9f6d215ceb3 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -1,5 +1,5 @@
# Store object full path in separate table for easy lookup and uniq validation
-# Object must have path db field and respond to full_path and full_path_changed? methods.
+# Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable
extend ActiveSupport::Concern
@@ -9,7 +9,13 @@ module Routable
validates_associated :route
validates :route, presence: true
- before_validation :update_route_path, if: :full_path_changed?
+ scope :with_route, -> { includes(:route) }
+
+ before_validation do
+ if full_path_changed? || full_name_changed?
+ prepare_route
+ end
+ end
end
class_methods do
@@ -77,10 +83,62 @@ module Routable
end
end
+ def full_name
+ if route && route.name.present?
+ @full_name ||= route.name
+ else
+ update_route if persisted?
+
+ build_full_name
+ end
+ end
+
+ def full_path
+ if route && route.path.present?
+ @full_path ||= route.path
+ else
+ update_route if persisted?
+
+ build_full_path
+ end
+ end
+
private
- def update_route_path
+ def full_name_changed?
+ name_changed? || parent_changed?
+ end
+
+ def full_path_changed?
+ path_changed? || parent_changed?
+ end
+
+ def build_full_name
+ if parent && name
+ parent.human_name + ' / ' + name
+ else
+ name
+ end
+ end
+
+ def build_full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def update_route
+ prepare_route
+ route.save
+ end
+
+ def prepare_route
route || build_route(source: self)
- route.path = full_path
+ route.path = build_full_path
+ route.name = build_full_name
+ @full_path = nil
+ @full_name = nil
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 1aa97debe42..423ae98a60e 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam
+ attr_accessor :spam_log
after_validation :check_for_spam, on: :create
@@ -34,7 +35,18 @@ module Spammable
end
def check_for_spam
- self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
+ error_msg = if Gitlab::Recaptcha.enabled?
+ "Your #{spammable_entity_type} has been recognized as spam. "\
+ "You can still submit it by solving Captcha."
+ else
+ "Your #{spammable_entity_type} has been recognized as spam and has been discarded."
+ end
+
+ self.errors.add(:base, error_msg) if spam?
+ end
+
+ def spammable_entity_type
+ self.class.name.underscore
end
def spam_title
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 040e3a2884e..9cf83440784 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
- has_many :timelogs, as: :trackable, dependent: :destroy
+ has_many :timelogs, dependent: :destroy
end
def spend_time(options)
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 91d85c2279b..afad001d50f 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
- def stoppable?
+ def stop_action?
stop_action.present?
end
diff --git a/app/models/directly_addressed_user.rb b/app/models/directly_addressed_user.rb
new file mode 100644
index 00000000000..0d519c6ac22
--- /dev/null
+++ b/app/models/directly_addressed_user.rb
@@ -0,0 +1,7 @@
+class DirectlyAddressedUser
+ class << self
+ def reference_pattern
+ User.reference_pattern
+ end
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 652abf18a8a..1a21b5e52b5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,11 +1,13 @@
class Environment < ActiveRecord::Base
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
belongs_to :project, required: true, validate: true
- has_many :deployments
+ has_many :deployments, dependent: :destroy
+ has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
@@ -36,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
+ scope :order_by_last_deployed_at, -> do
+ max_deployment_id_sql =
+ Deployment.select(Deployment.arel_table[:id].maximum).
+ where(Deployment.arel_table[:environment_id].eq(arel_table[:id])).
+ to_sql
+ order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC'))
+ end
state_machine :state, initial: :available do
event :start do
@@ -61,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref)
end
- def last_deployment
- deployments.last
- end
-
def nullify_external_url
self.external_url = nil if self.external_url.blank?
end
@@ -86,6 +91,10 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit)
end
+ def last_deployed_at
+ last_deployment.try(:created_at)
+ end
+
def update_merge_request_metrics?
(environment_type || name) == "production"
end
@@ -109,15 +118,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
- def stoppable?
+ def stop_action?
available? && stop_action.present?
end
- def stop!(current_user)
- return unless stoppable?
+ def stop_with_action!(current_user)
+ return unless available?
- stop
- stop_action.play(current_user)
+ stop!
+ stop_action&.play(current_user)
end
def actions_for(environment)
@@ -148,21 +157,37 @@ class Environment < ActiveRecord::Base
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
self.slug = slugified
end
+ def external_url_for(path, commit_sha)
+ return unless self.external_url
+
+ public_path = project.public_path_for_source_path(path, commit_sha)
+ return unless public_path
+
+ [external_url, public_path].join('/')
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/group.rb b/app/models/group.rb
index 4cdfd022094..cc6624ff4aa 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -197,11 +197,16 @@ class Group < Namespace
end
def refresh_members_authorized_projects
- UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
+ UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
+ execute
+ end
+
+ def user_ids_for_project_authorizations
+ users_with_parents.pluck(:id)
end
def members_with_parents
- GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
+ GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
end
def users_with_parents
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 7b6db2634b7..86d38e5468b 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -9,7 +9,7 @@ class GroupMilestone < GlobalMilestone
def self.build(group, projects, title)
super(projects, title).tap do |milestone|
- milestone.group = group if milestone
+ milestone&.group = group
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 65638d9a299..d8826b65fcc 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -97,10 +97,11 @@ class Issue < ActiveRecord::Base
end
end
- def to_reference(from_project = nil, full: false)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
end
def referenced_merge_requests(current_user = nil)
diff --git a/app/models/list.rb b/app/models/list.rb
index 065d75bd1dc..1e5da7f4dd4 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, done: 2 }
+ enum list_type: { label: 1, done: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/member.rb b/app/models/member.rb
index 26a6054e00d..d07f270b757 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -47,6 +47,7 @@ class Member < ActiveRecord::Base
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
scope :request, -> { where.not(requested_at: nil) }
+ scope :non_request, -> { where(requested_at: nil) }
scope :has_access, -> { active.where('access_level > 0') }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 6753504acff..c0d4dd0197f 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -179,10 +179,11 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
- def to_reference(from_project = nil, full: false)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ "#{project.to_reference(from, full: full)}#{reference}"
end
def first_commit
@@ -545,7 +546,7 @@ class MergeRequest < ActiveRecord::Base
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
- def cache_merge_request_closes_issues!(current_user = self.author)
+ def cache_merge_request_closes_issues!(current_user)
return if project.has_external_issue_tracker?
transaction do
@@ -557,10 +558,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- def closes_issue?(issue)
- closes_issues.include?(issue)
- end
-
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
@@ -574,13 +571,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- def issues_mentioned_but_not_closing(current_user = self.author)
+ def issues_mentioned_but_not_closing(current_user)
return [] unless target_branch == project.default_branch
ext = Gitlab::ReferenceExtractor.new(project, current_user)
ext.analyze(description)
- ext.issues - closes_issues
+ ext.issues - closes_issues(current_user)
end
def target_project_path
@@ -714,18 +711,22 @@ class MergeRequest < ActiveRecord::Base
!head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end
- def environments
+ def environments_for(current_user)
return [] unless diff_head_commit
- @environments ||= begin
- target_envs = target_project.environments_for(
- target_branch, commit: diff_head_commit, with_tags: true)
+ @environments ||= Hash.new do |h, current_user|
+ envs = EnvironmentsFinder.new(target_project, current_user,
+ ref: target_branch, commit: diff_head_commit, with_tags: true).execute
- source_envs = source_project.environments_for(
- source_branch, commit: diff_head_commit) if source_project
+ if source_project
+ envs.concat EnvironmentsFinder.new(source_project, current_user,
+ ref: source_branch, commit: diff_head_commit).execute
+ end
- (target_envs.to_a + source_envs.to_a).uniq
+ h[current_user] = envs.uniq
end
+
+ @environments[current_user]
end
def state_human_name
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index dadb81f9b6e..70bad2a4396 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base
# When compare merge request versions we want diff A..B instead of A...B
# so we handle cases when user does squash and rebase of the commits between versions.
# For this reason we set straight to true by default.
- CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight)
+ CompareService.new(project, head_commit_sha)
+ .execute(project, sha, straight: straight)
end
def commits_count
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 67d8c1c2e4c..6de4d08fc28 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -7,6 +7,11 @@ class Namespace < ActiveRecord::Base
include Gitlab::CurrentSettings
include Routable
+ # Prevent users from creating unreasonably deep level of nesting.
+ # The number 20 was taken based on maximum nesting level of
+ # Android repo (15) + some extra backup.
+ NUMBER_OF_ANCESTORS_ALLOWED = 20
+
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
@@ -29,6 +34,8 @@ class Namespace < ActiveRecord::Base
length: { maximum: 255 },
namespace: true
+ validate :nesting_level_allowed
+
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
@@ -130,6 +137,7 @@ class Namespace < ActiveRecord::Base
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
+ Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
remove_exports!
@@ -169,31 +177,14 @@ class Namespace < ActiveRecord::Base
Gitlab.config.lfs.enabled
end
- def full_path
- if parent
- parent.full_path + '/' + path
- else
- path
- end
- end
-
def shared_runners_enabled?
projects.with_shared_runners.any?
end
- def full_name
- @full_name ||=
- if parent
- parent.full_name + ' / ' + name
- else
- name
- end
- end
-
# Scopes the model on ancestors of the record
def ancestors
if parent_id
- path = route.path
+ path = route ? route.path : full_path
paths = []
until path.blank?
@@ -212,6 +203,14 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
+ def user_ids_for_project_authorizations
+ [owner_id]
+ end
+
+ def parent_changed?
+ parent_id_changed?
+ end
+
private
def repository_storage_paths
@@ -250,10 +249,6 @@ class Namespace < ActiveRecord::Base
find_each(&:refresh_members_authorized_projects)
end
- def full_path_changed?
- path_changed? || parent_id_changed?
- end
-
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
@@ -269,4 +264,10 @@ class Namespace < ActiveRecord::Base
path_was
end
end
+
+ def nesting_level_allowed
+ if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
+ errors.add(:parent_id, "has too deep level of nesting")
+ end
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index bf090a0438c..029fe667a45 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -108,6 +108,12 @@ class Note < ActiveRecord::Base
Discussion.for_diff_notes(active_notes).
map { |d| [d.line_code, d] }.to_h
end
+
+ def count_for_collection(ids, type)
+ user.select('noteable_id', 'COUNT(*) as count').
+ group(:noteable_id).
+ where(noteable_type: type, noteable_id: ids)
+ end
end
def cross_reference?
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
new file mode 100644
index 00000000000..0b9ebf1ffe2
--- /dev/null
+++ b/app/models/pages_domain.rb
@@ -0,0 +1,119 @@
+class PagesDomain < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, hostname: true
+ validates_uniqueness_of :domain, case_sensitive: false
+ validates :certificate, certificate: true, allow_nil: true, allow_blank: true
+ validates :key, certificate_key: true, allow_nil: true, allow_blank: true
+
+ validate :validate_pages_domain
+ validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
+
+ attr_encrypted :key,
+ mode: :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ key: Gitlab::Application.secrets.db_key_base,
+ algorithm: 'aes-256-cbc'
+
+ after_create :update
+ after_save :update
+ after_destroy :update
+
+ def to_param
+ domain
+ end
+
+ def url
+ return unless domain
+
+ if certificate
+ "https://#{domain}"
+ else
+ "http://#{domain}"
+ end
+ end
+
+ def has_matching_key?
+ return false unless x509
+ return false unless pkey
+
+ # We compare the public key stored in certificate with public key from certificate key
+ x509.check_private_key(pkey)
+ end
+
+ def has_intermediates?
+ return false unless x509
+
+ # self-signed certificates doesn't have the certificate chain
+ return true if x509.verify(x509.public_key)
+
+ store = OpenSSL::X509::Store.new
+ store.set_default_paths
+
+ # This forces to load all intermediate certificates stored in `certificate`
+ Tempfile.open('certificate_chain') do |f|
+ f.write(certificate)
+ f.flush
+ store.add_file(f.path)
+ end
+
+ store.verify(x509)
+ rescue OpenSSL::X509::StoreError
+ false
+ end
+
+ def expired?
+ return false unless x509
+ current = Time.new
+ current < x509.not_before || x509.not_after < current
+ end
+
+ def subject
+ return unless x509
+ x509.subject.to_s
+ end
+
+ def certificate_text
+ @certificate_text ||= x509.try(:to_text)
+ end
+
+ private
+
+ def update
+ ::Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def validate_matching_key
+ unless has_matching_key?
+ self.errors.add(:key, "doesn't match the certificate")
+ end
+ end
+
+ def validate_intermediates
+ unless has_intermediates?
+ self.errors.add(:certificate, 'misses intermediates')
+ end
+ end
+
+ def validate_pages_domain
+ return unless domain
+ if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
+ self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
+ end
+ end
+
+ def x509
+ return unless certificate
+ @x509 ||= OpenSSL::X509::Certificate.new(certificate)
+ rescue OpenSSL::X509::CertificateError
+ nil
+ end
+
+ def pkey
+ return unless key
+ @pkey ||= OpenSSL::PKey::RSA.new(key)
+ rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError
+ nil
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 59faf35e051..aa408b4556e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -53,6 +53,8 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
+ after_destroy :remove_pages
+
# update visibility_level of forks
after_update :update_forks_visibility_level
def update_forks_visibility_level
@@ -148,6 +150,7 @@ class Project < ActiveRecord::Base
has_many :lfs_objects, through: :lfs_objects_projects
has_many :project_group_links, dependent: :destroy
has_many :invited_groups, through: :project_group_links, source: :group
+ has_many :pages_domains, dependent: :destroy
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source
@@ -225,6 +228,12 @@ class Project < ActiveRecord::Base
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
+ scope :inside_path, ->(path) do
+ # We need routes alias rs for JOIN so it does not conflict with
+ # includes(:route) which we use in ProjectsFinder.
+ joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'").
+ where('rs.path LIKE ?', "#{path}/%")
+ end
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
@@ -369,10 +378,6 @@ class Project < ActiveRecord::Base
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
-
- # Add alias for Routable method for compatibility with old code.
- # In future all calls `find_with_namespace` should be replaced with `find_by_full_path`
- alias_method :find_with_namespace, :find_by_full_path
end
def lfs_enabled?
@@ -464,7 +469,7 @@ class Project < ActiveRecord::Base
def reset_cache_and_import_attrs
ProjectCacheWorker.perform_async(self.id)
- self.import_data.destroy if self.import_data
+ self.import_data&.destroy
end
def import_url=(value)
@@ -591,10 +596,11 @@ class Project < ActiveRecord::Base
end
end
- def to_reference(from_project = nil, full: false)
- if full || cross_namespace_reference?(from_project)
+ # `from` argument can be a Namespace or Project.
+ def to_reference(from = nil, full: false)
+ if full || cross_namespace_reference?(from)
path_with_namespace
- elsif cross_project_reference?(from_project)
+ elsif cross_project_reference?(from)
path
end
end
@@ -809,26 +815,6 @@ class Project < ActiveRecord::Base
end
end
- def name_with_namespace
- @name_with_namespace ||= begin
- if namespace
- namespace.human_name + ' / ' + name
- else
- name
- end
- end
- end
- alias_method :human_name, :name_with_namespace
-
- def full_path
- if namespace && path
- namespace.full_path + '/' + path
- else
- path
- end
- end
- alias_method :path_with_namespace, :full_path
-
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hook.async_execute(data, hooks_scope.to_s)
@@ -957,6 +943,7 @@ class Project < ActiveRecord::Base
Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
+ Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path)
end
# Expires various caches before a project is renamed.
@@ -1158,6 +1145,45 @@ class Project < ActiveRecord::Base
ensure_runners_token!
end
+ def pages_deployed?
+ Dir.exist?(public_pages_path)
+ end
+
+ def pages_url
+ # The hostname always needs to be in downcased
+ # All web servers convert hostname to lowercase
+ host = "#{namespace.path}.#{Settings.pages.host}".downcase
+
+ # The host in URL always needs to be downcased
+ url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix|
+ "#{prefix}#{namespace.path}."
+ end.downcase
+
+ # If the project path is the same as host, we serve it as group page
+ return url if host == path
+
+ "#{url}/#{path}"
+ end
+
+ def pages_path
+ File.join(Settings.pages.path, path_with_namespace)
+ end
+
+ def public_pages_path
+ File.join(pages_path, 'public')
+ end
+
+ def remove_pages
+ # 1. We rename pages to temporary directory
+ # 2. We wait 5 minutes, due to NFS caching
+ # 3. We asynchronously remove pages with force
+ temp_path = "#{path}.#{SecureRandom.hex}.deleted"
+
+ if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path)
+ PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path)
+ end
+ end
+
def wiki
@wiki ||= ProjectWiki.new(self, self.owner)
end
@@ -1265,47 +1291,62 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
- def environments_for(ref, commit: nil, with_tags: false)
- deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?'
+ def route_map_for(commit_sha)
+ @route_maps_by_commit ||= Hash.new do |h, sha|
+ h[sha] = begin
+ data = repository.route_map_for(sha)
+ next unless data
- environment_ids = deployments
- .where(deployments_query, ref.to_s)
- .group(:environment_id)
- .select(:environment_id)
+ Gitlab::RouteMap.new(data)
+ rescue Gitlab::RouteMap::FormatError
+ nil
+ end
+ end
- environments_found = environments.available
- .where(id: environment_ids).to_a
+ @route_maps_by_commit[commit_sha]
+ end
- return environments_found unless commit
+ def public_path_for_source_path(path, commit_sha)
+ map = route_map_for(commit_sha)
+ return unless map
- environments_found.select do |environment|
- environment.includes_commit?(commit)
- end
+ map.public_path_for_source_path(path)
end
- def environments_recently_updated_on_branch(branch)
- environments_for(branch).select do |environment|
- environment.recently_updated_on_branch?(branch)
- end
+ def parent
+ namespace
+ end
+
+ def parent_changed?
+ namespace_id_changed?
end
+ alias_method :name_with_namespace, :full_name
+ alias_method :human_name, :full_name
+ alias_method :path_with_namespace, :full_path
+
private
+ def cross_namespace_reference?(from)
+ case from
+ when Project
+ namespace != from.namespace
+ when Namespace
+ namespace != from
+ end
+ end
+
# Check if a reference is being done cross-project
- #
- # from_project - Refering Project object
- def cross_project_reference?(from_project)
- from_project && self != from_project
+ def cross_project_reference?(from)
+ return true if from.is_a?(Namespace)
+
+ from && self != from
end
def pushes_since_gc_redis_key
"projects/#{id}/pushes_since_gc"
end
- def cross_namespace_reference?(from_project)
- from_project && namespace != from_project.namespace
- end
-
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
@@ -1322,10 +1363,6 @@ class Project < ActiveRecord::Base
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end
- def full_path_changed?
- path_changed? || namespace_id_changed?
- end
-
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
@@ -1345,6 +1382,6 @@ class Project < ActiveRecord::Base
def pending_delete_twin
return false unless path
- Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
+ Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace)
end
end
diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb
index 2bcff541cc0..8b5bc24fd3c 100644
--- a/app/models/project_services/chat_slash_commands_service.rb
+++ b/app/models/project_services/chat_slash_commands_service.rb
@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
@@ -31,13 +31,13 @@ class ChatSlashCommandsService < Service
return unless valid_token?(params[:token])
user = find_chat_user(params)
- unless user
+
+ if user
+ Gitlab::ChatCommands::Command.new(project, user, params).execute
+ else
url = authorize_chat_name_url(params)
- return presenter.authorize_chat_name(url)
+ Gitlab::ChatCommands::Presenters::Access.new(url).authorize
end
-
- Gitlab::ChatCommands::Command.new(project, user,
- params).execute
end
private
@@ -49,8 +49,4 @@ class ChatSlashCommandsService < Service
def authorize_chat_name_url(params)
ChatNames::AuthorizeUserService.new(self, params).execute
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2ac76e97de0..eef403dba92 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -60,9 +60,9 @@ class JiraService < IssueTrackerService
end
def help
- 'You need to configure JIRA before enabling this service. For more details
+ "You need to configure JIRA before enabling this service. For more details
read the
- [JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
+ [JIRA service documentation](#{help_page_url('project_services/jira')})."
end
def title
@@ -250,21 +250,11 @@ class JiraService < IssueTrackerService
end
end
- # 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: {
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index fa3cedc4354..f2f019c43bb 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -1,4 +1,5 @@
class KubernetesService < DeploymentService
+ include Gitlab::CurrentSettings
include Gitlab::Kubernetes
include ReactiveCaching
@@ -110,7 +111,7 @@ class KubernetesService < DeploymentService
pods = data.fetch(:pods, nil)
filter_pods(pods, app: environment.slug).
flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }.
- map { |terminal| add_terminal_auth(terminal, token, ca_pem) }
+ each { |terminal| add_terminal_auth(terminal, terminal_auth) }
end
end
@@ -170,4 +171,12 @@ class KubernetesService < DeploymentService
url.to_s
end
+
+ def terminal_auth
+ {
+ token: token,
+ ca_pem: ca_pem,
+ max_session_time: current_application_settings.terminal_max_session_time
+ }
+ end
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 50a011db74e..56f42d63b2d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
- 'Mattermost Command'
+ 'Mattermost slash commands'
end
def description
- "Perform common operations on GitLab in Mattermost"
+ "Perform common operations in Mattermost"
end
def self.to_param
@@ -28,8 +28,8 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
[false, e.message]
end
- def list_teams(user)
- Mattermost::Team.new(user).all
+ def list_teams(current_user)
+ [Mattermost::Team.new(current_user).all, nil]
rescue Mattermost::Error => e
[[], e.message]
end
diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb
index c34991e4262..2182c1c7e4b 100644
--- a/app/models/project_services/slack_slash_commands_service.rb
+++ b/app/models/project_services/slack_slash_commands_service.rb
@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
- 'Slack Command'
+ 'Slack slash commands'
end
def description
- "Perform common operations on GitLab in Slack"
+ "Perform common operations in Slack"
end
def self.to_param
diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb
index 25b5d777641..9bb456eee24 100644
--- a/app/models/project_snippet.rb
+++ b/app/models/project_snippet.rb
@@ -9,4 +9,8 @@ class ProjectSnippet < Snippet
participant :author
participant :notes_with_associations
+
+ def check_for_spam?
+ super && project.public?
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index d77b7692d75..d2d92a064a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -5,7 +5,7 @@ class Repository
attr_accessor :path_with_namespace, :project
- class CommitError < StandardError; end
+ CommitError = Class.new(StandardError)
# Methods that cache data from the Git repository.
#
@@ -64,10 +64,6 @@ class Repository
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
end
- def update_autocrlf_option
- raw_repository.autocrlf = :input if raw_repository.autocrlf != :input
- end
-
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -168,63 +164,46 @@ class Repository
tags.find { |tag| tag.name == name }
end
- def add_branch(user, branch_name, target)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- target = commit(target).try(:id)
+ def add_branch(user, branch_name, ref)
+ newrev = commit(ref).try(:sha)
- return false unless target
+ return false unless newrev
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- update_ref!(ref, target, oldrev)
- end
+ GitOperationService.new(user, self).add_branch(branch_name, newrev)
after_create_branch
find_branch(branch_name)
end
def add_tag(user, tag_name, target, message = nil)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
- target = commit(target).try(:id)
-
- return false unless target
-
+ newrev = commit(target).try(:id)
options = { message: message, tagger: user_to_committer(user) } if message
- GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service|
- raw_tag = rugged.tags.create(tag_name, target, options)
- service.newrev = raw_tag.target_id
- end
+ return false unless newrev
+
+ GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end
def rm_branch(user, branch_name)
before_remove_branch
-
branch = find_branch(branch_name)
- oldrev = branch.try(:dereferenced_target).try(:id)
- newrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
- end
+ GitOperationService.new(user, self).rm_branch(branch)
after_remove_branch
true
end
- def rm_tag(tag_name)
+ def rm_tag(user, tag_name)
before_remove_tag
+ tag = find_tag(tag_name)
- begin
- rugged.tags.delete(tag_name)
- true
- rescue Rugged::ReferenceError
- false
- end
+ GitOperationService.new(user, self).rm_tag(tag)
+
+ after_remove_tag
+ true
end
def ref_names
@@ -241,21 +220,6 @@ class Repository
false
end
- def update_ref!(name, newrev, oldrev)
- # We use 'git update-ref' because libgit2/rugged currently does not
- # 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(#{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
-
- return if status.zero?
-
- raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
- end
-
# Makes sure a commit is kept around when Git garbage collection runs.
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
@@ -435,6 +399,11 @@ class Repository
repository_event(:remove_tag)
end
+ # Runs code after removing a tag.
+ def after_remove_tag
+ expire_tags_cache
+ end
+
def before_import
expire_content_cache
end
@@ -495,6 +464,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
end
def blob_by_oid(oid)
@@ -779,121 +750,132 @@ class Repository
@tags ||= raw_repository.tags
end
- def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- raw_repository.mkdir(path, options)
- end
- end
-
- def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: update
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+ # rubocop:disable Metrics/ParameterLists
+ def commit_dir(
+ user, path,
+ message:, branch_name:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ check_tree_entry_for_dir(branch_name, path)
- Gitlab::Git::Blob.commit(raw_repository, options)
+ if start_branch_name
+ start_project.repository.
+ check_tree_entry_for_dir(start_branch_name, path)
end
- end
- def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- content: content,
- path: path,
- update: true
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
-
- if previous_path && previous_path != path
- options[:file][:previous_path] = previous_path
- Gitlab::Git::Blob.rename(raw_repository, options)
- else
- Gitlab::Git::Blob.commit(raw_repository, options)
+ commit_file(
+ user,
+ "#{path}/.gitkeep",
+ '',
+ message: message,
+ branch_name: branch_name,
+ update: false,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project)
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def commit_file(
+ user, path, content,
+ message:, branch_name:, update: true,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ unless update
+ error_message = "Filename already exists; update not allowed"
+
+ if tree_entry_at(branch_name, path)
+ raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
end
- end
- end
-
- def remove_file(user, path, message, branch, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
- options = {
- commit: {
- branch: ref,
- message: message,
- update_ref: false
- },
- file: {
- path: path
- }
- }
-
- options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
- Gitlab::Git::Blob.remove(raw_repository, options)
+ if start_branch_name &&
+ start_project.repository.tree_entry_at(start_branch_name, path)
+ raise Gitlab::Git::Repository::InvalidBlobName.new(error_message)
+ end
end
- end
- def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
- update_branch_with_hooks(user, branch) do |ref|
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: :create,
+ file_path: path,
+ content: content }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def update_file(
+ user, path, content,
+ message:, branch_name:, previous_path:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ action = if previous_path && previous_path != path
+ :move
+ else
+ :update
+ end
+
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: action,
+ file_path: path,
+ content: content,
+ previous_path: previous_path }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def remove_file(
+ user, path,
+ message:, branch_name:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ multi_action(
+ user: user,
+ message: message,
+ branch_name: branch_name,
+ author_email: author_email,
+ author_name: author_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project,
+ actions: [{ action: :delete,
+ file_path: path }])
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ # rubocop:disable Metrics/ParameterLists
+ def multi_action(
+ user:, branch_name:, message:, actions:,
+ author_email: nil, author_name: nil,
+ start_branch_name: nil, start_project: project)
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
index = rugged.index
- parents = []
- branch = find_branch(ref)
- if branch
- last_commit = branch.dereferenced_target
- index.read_tree(last_commit.raw_commit.tree)
- parents = [last_commit.sha]
- end
+ parents = if start_commit
+ index.read_tree(start_commit.raw_commit.tree)
+ [start_commit.sha]
+ else
+ []
+ end
- actions.each do |action|
- case action[:action]
- when :create, :update, :move
- mode =
- case action[:action]
- when :update
- index.get(action[:file_path])[:mode]
- when :move
- index.get(action[:previous_path])[:mode]
- end
- mode ||= 0o100644
-
- index.remove(action[:previous_path]) if action[:action] == :move
-
- content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
- oid = rugged.write(content, :blob)
-
- index.add(path: action[:file_path], oid: oid, mode: mode)
- when :delete
- index.remove(action[:file_path])
- end
+ actions.each do |act|
+ git_action(index, act)
end
options = {
@@ -906,6 +888,7 @@ class Repository
Rugged::Commit.create(rugged, options)
end
end
+ # rubocop:enable Metrics/ParameterLists
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
@@ -918,7 +901,7 @@ class Repository
end
def user_to_committer(user)
- Gitlab::Git::committer_hash(email: user.email, name: user.name)
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
end
def can_be_merged?(source_sha, target_branch)
@@ -932,17 +915,18 @@ class Repository
end
end
- def merge(user, merge_request, options = {})
- our_commit = rugged.branches[merge_request.target_branch].target
- their_commit = rugged.lookup(merge_request.diff_head_sha)
+ def merge(user, source, merge_request, options = {})
+ GitOperationService.new(user, self).with_branch(
+ merge_request.target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source
- raise "Invalid merge target" if our_commit.nil?
- raise "Invalid merge source" if their_commit.nil?
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
- merge_index = rugged.merge_commits(our_commit, their_commit)
- return false if merge_index.conflicts?
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
- update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
@@ -952,34 +936,48 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
+ rescue Repository::CommitError # when merge_index.conflicts?
+ false
end
- def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- revert_tree_id ||= check_revert_content(commit, base_branch)
+ def revert(
+ user, commit, branch_name, revert_tree_id = nil,
+ start_branch_name: nil, start_project: project)
+ revert_tree_id ||= check_revert_content(commit, branch_name)
return false unless revert_tree_id
- update_branch_with_hooks(user, base_branch) do
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
+
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.revert_message(user),
author: committer,
committer: committer,
tree: revert_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
+ def cherry_pick(
+ user, commit, branch_name, cherry_pick_tree_id = nil,
+ start_branch_name: nil, start_project: project)
+ cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name)
return false unless cherry_pick_tree_id
- update_branch_with_hooks(user, base_branch) do
+ GitOperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_project: start_project) do |start_commit|
+
committer = user_to_committer(user)
- source_sha = Rugged::Commit.create(rugged,
+
+ Rugged::Commit.create(rugged,
message: commit.message,
author: {
email: commit.author_email,
@@ -988,22 +986,22 @@ class Repository
},
committer: committer,
tree: cherry_pick_tree_id,
- parents: [rugged.lookup(source_sha)])
+ parents: [start_commit.sha])
end
end
- def resolve_conflicts(user, branch, params)
- update_branch_with_hooks(user, branch) do
+ def resolve_conflicts(user, branch_name, params)
+ GitOperationService.new(user, self).with_branch(branch_name) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
end
end
- def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << { mainline: 1 } if commit.merge_commit?
+ def check_revert_content(target_commit, branch_name)
+ source_sha = commit(branch_name).sha
+ args = [target_commit.sha, source_sha]
+ args << { mainline: 1 } if target_commit.merge_commit?
revert_index = rugged.revert_commit(*args)
return false if revert_index.conflicts?
@@ -1014,10 +1012,10 @@ class Repository
tree_id
end
- def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).dereferenced_target.sha
- args = [commit.id, source_sha]
- args << 1 if commit.merge_commit?
+ def check_cherry_pick_content(target_commit, branch_name)
+ source_sha = commit(branch_name).sha
+ args = [target_commit.sha, source_sha]
+ args << 1 if target_commit.merge_commit?
cherry_pick_index = rugged.cherrypick_commit(*args)
return false if cherry_pick_index.conflicts?
@@ -1075,6 +1073,28 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
end
+ def with_repo_branch_commit(start_repository, start_branch_name)
+ branch_name_or_sha =
+ if start_repository == self
+ start_branch_name
+ else
+ tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
+
+ fetch_ref(
+ start_repository.path_to_repo,
+ "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
+ tmp_ref
+ )
+
+ start_repository.commit(start_branch_name).sha
+ end
+
+ yield(commit(branch_name_or_sha))
+
+ ensure
+ rugged.references.delete(tmp_ref) if tmp_ref
+ 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)
@@ -1084,39 +1104,6 @@ class Repository
fetch_ref(path_to_repo, ref, ref_path)
end
- def update_branch_with_hooks(current_user, branch)
- update_autocrlf_option
-
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
- target_branch = find_branch(branch)
- was_empty = empty?
-
- # Make commit
- newrev = yield(ref)
-
- unless newrev
- raise CommitError.new('Failed to create commit')
- end
-
- if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
- oldrev = Gitlab::Git::BLANK_SHA
- else
- oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
- end
-
- GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- update_ref!(ref, newrev, oldrev)
-
- if was_empty || !target_branch
- # If repo was empty expire cache
- after_create if was_empty
- after_create_branch
- end
- end
-
- newrev
- end
-
def ls_files(ref)
actual_ref = ref || root_ref
raw_repository.ls_files(actual_ref)
@@ -1175,8 +1162,92 @@ class Repository
end
end
+ def route_map_for(sha)
+ blob_data_at(sha, '.gitlab/route-map.yml')
+ end
+
+ def gitlab_ci_yml_for(sha)
+ blob_data_at(sha, '.gitlab-ci.yml')
+ end
+
+ protected
+
+ def tree_entry_at(branch_name, path)
+ branch_exists?(branch_name) &&
+ # tree_entry is private
+ raw_repository.send(:tree_entry, commit(branch_name), path)
+ end
+
+ def check_tree_entry_for_dir(branch_name, path)
+ return unless branch_exists?(branch_name)
+
+ entry = tree_entry_at(branch_name, path)
+
+ return unless entry
+
+ if entry[:type] == :blob
+ raise Gitlab::Git::Repository::InvalidBlobName.new(
+ "Directory already exists as a file")
+ else
+ raise Gitlab::Git::Repository::InvalidBlobName.new(
+ "Directory already exists")
+ end
+ end
+
private
+ def blob_data_at(sha, path)
+ blob = blob_at(sha, path)
+ return unless blob
+
+ blob.load_all_data!(self)
+ blob.data
+ end
+
+ def git_action(index, action)
+ path = normalize_path(action[:file_path])
+
+ if action[:action] == :move
+ previous_path = normalize_path(action[:previous_path])
+ end
+
+ case action[:action]
+ when :create, :update, :move
+ mode =
+ case action[:action]
+ when :update
+ index.get(path)[:mode]
+ when :move
+ index.get(previous_path)[:mode]
+ end
+ mode ||= 0o100644
+
+ index.remove(previous_path) if action[:action] == :move
+
+ content = if action[:encoding] == 'base64'
+ Base64.decode64(action[:content])
+ else
+ action[:content]
+ end
+
+ oid = rugged.write(content, :blob)
+
+ index.add(path: path, oid: oid, mode: mode)
+ when :delete
+ index.remove(path)
+ end
+ end
+
+ def normalize_path(path)
+ pathname = Gitlab::Git::PathHelper.normalize_path(path)
+
+ if pathname.each_filename.include?('..')
+ raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
+ end
+
+ pathname.to_s
+ end
+
def refs_directory_exists?
return false unless path_with_namespace
diff --git a/app/models/route.rb b/app/models/route.rb
index dd171fdb069..73574a6206b 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,16 +8,27 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- after_update :rename_descendants, if: :path_changed?
+ after_update :rename_descendants
def rename_descendants
- # We update each row separately because MySQL does not have regexp_replace.
- # rubocop:disable Rails/FindEach
- Route.where('path LIKE ?', "#{path_was}/%").each do |route|
- # Note that update column skips validation and callbacks.
- # We need this to avoid recursive call of rename_descendants method
- route.update_column(:path, route.path.sub(path_was, path))
+ if path_changed? || name_changed?
+ descendants = Route.where('path LIKE ?', "#{path_was}/%")
+
+ descendants.each do |route|
+ attributes = {}
+
+ if path_changed? && route.path.present?
+ attributes[:path] = route.path.sub(path_was, path)
+ end
+
+ if name_changed? && route.name.present?
+ attributes[:name] = route.name.sub(name_was, name)
+ end
+
+ # Note that update_columns skips validation and callbacks.
+ # We need this to avoid recursive call of rename_descendants method
+ route.update_columns(attributes) unless attributes.empty?
+ end
end
- # rubocop:enable Rails/FindEach
end
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 771a7350556..2665a7249a3 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable
include Awardable
include Mentionable
+ include Spammable
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content
@@ -17,7 +18,7 @@ class Snippet < ActiveRecord::Base
default_content_html_invalidator || file_name_changed?
end
- default_value_for :visibility_level, Snippet::PRIVATE
+ default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility }
belongs_to :author, class_name: 'User'
belongs_to :project
@@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base
participant :author
participant :notes_with_associations
+ attr_spammable :title, spam_title: true
+ attr_spammable :content, spam_description: true
+
def self.reference_prefix
'$'
end
@@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base
notes.includes(:author)
end
+ def check_for_spam?
+ public?
+ end
+
+ def spammable_entity_type
+ 'snippet'
+ end
+
class << self
# Searches for snippets with a matching title or file name.
#
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f768c4e3da5..e166cf69703 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -1,6 +1,22 @@
class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
+ validate :issuable_id_is_present
- belongs_to :trackable, polymorphic: true
+ belongs_to :issue
+ belongs_to :merge_request
belongs_to :user
+
+ def issuable
+ issue || merge_request
+ end
+
+ private
+
+ def issuable_id_is_present
+ if issue_id && merge_request_id
+ errors.add(:base, 'Only Issue ID or Merge Request ID is required')
+ elsif issuable.nil?
+ errors.add(:base, 'Issue or Merge Request ID is required')
+ end
+ end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 4c99aa0d3be..3dda7948d0b 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,12 +1,13 @@
class Todo < ActiveRecord::Base
include Sortable
- ASSIGNED = 1
- MENTIONED = 2
- BUILD_FAILED = 3
- MARKED = 4
- APPROVAL_REQUIRED = 5 # This is an EE-only feature
- UNMERGEABLE = 6
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
+ MARKED = 4
+ APPROVAL_REQUIRED = 5 # This is an EE-only feature
+ UNMERGEABLE = 6
+ DIRECTLY_ADDRESSED = 7
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -14,7 +15,8 @@ class Todo < ActiveRecord::Base
BUILD_FAILED => :build_failed,
MARKED => :marked,
APPROVAL_REQUIRED => :approval_required,
- UNMERGEABLE => :unmergeable
+ UNMERGEABLE => :unmergeable,
+ DIRECTLY_ADDRESSED => :directly_addressed
}
belongs_to :author, class_name: "User"
@@ -103,9 +105,9 @@ class Todo < ActiveRecord::Base
def target_reference
if for_commit?
- target.short_id
+ target.reference_link_text(full: true)
else
- target.to_reference
+ target.to_reference(full: true)
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 54f5388eb2c..867e61af56a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -83,8 +83,6 @@ class User < ActiveRecord::Base
has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
- has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
- has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
@@ -94,6 +92,9 @@ class User < ActiveRecord::Base
has_many :notification_settings, dependent: :destroy
has_many :award_emoji, dependent: :destroy
+ has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
+ has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+
#
# Validations
#
@@ -118,7 +119,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
- before_validation :signup_domain_valid?, on: :create
+ before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -166,6 +167,15 @@ class User < ActiveRecord::Base
def blocked?
true
end
+
+ def active_for_authentication?
+ false
+ end
+
+ def inactive_message
+ "Your account has been blocked. Please contact your GitLab " \
+ "administrator if you think this is an error."
+ end
end
end
@@ -304,7 +314,7 @@ class User < ActiveRecord::Base
def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
- personal_access_token.user if personal_access_token
+ personal_access_token&.user
end
# Returns a user for the given SSH key.
@@ -903,6 +913,21 @@ class User < ActiveRecord::Base
end
end
+ def access_level
+ if admin?
+ :admin
+ else
+ :regular
+ end
+ end
+
+ def access_level=(new_level)
+ new_level = new_level.to_s
+ return unless %w(admin regular).include?(new_level)
+
+ self.admin = (new_level == 'admin')
+ end
+
private
def ci_projects_union
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2f4f92846b4..6347b274341 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -95,9 +95,7 @@ class WikiPage
# The raw content of this page.
def content
- @attributes[:content] ||= if @page
- @page.text_data
- end
+ @attributes[:content] ||= @page&.text_data
end
# The hierarchy of the directory this page is contained in.
@@ -107,9 +105,7 @@ class WikiPage
# The processed/formatted content of this page.
def formatted_content
- @attributes[:formatted_content] ||= if @page
- @page.formatted_data
- end
+ @attributes[:formatted_content] ||= @page&.formatted_data
end
# The markup format for the page.
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 7b1752df0e1..8b25332b73c 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,8 +1,6 @@
module Ci
class BuildPolicy < CommitStatusPolicy
def rules
- can! :read_build if @subject.project.public_builds?
-
super
# If we can't read build we should also not have that
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 71ef8901932..f8594e29547 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -110,6 +110,9 @@ class ProjectPolicy < BasePolicy
can! :admin_pipeline
can! :admin_environment
can! :admin_deployment
+ can! :admin_pages
+ can! :read_pages
+ can! :update_pages
end
def public_access!
@@ -136,6 +139,7 @@ class ProjectPolicy < BasePolicy
can! :remove_fork_project
can! :destroy_merge_request
can! :destroy_issue
+ can! :remove_pages
end
def team_member_owner_access!
@@ -214,25 +218,7 @@ class ProjectPolicy < BasePolicy
def anonymous_rules
return unless project.public?
- can! :read_project
- can! :read_board
- can! :read_list
- can! :read_wiki
- can! :read_label
- can! :read_milestone
- can! :read_project_snippet
- can! :read_project_member
- can! :read_merge_request
- can! :read_note
- can! :read_pipeline
- can! :read_commit_status
- can! :read_container_image
- can! :download_code
- can! :download_wiki_code
- can! :read_cycle_analytics
-
- # NOTE: may be overridden by IssuePolicy
- can! :read_issue
+ base_readonly_access!
# Allow to read builds by anonymous user if guests are allowed
can! :read_build if project.public_builds?
@@ -265,4 +251,31 @@ class ProjectPolicy < BasePolicy
:"admin_#{name}"
]
end
+
+ private
+
+ # A base set of abilities for read-only users, which
+ # is then augmented as necessary for anonymous and other
+ # read-only users.
+ def base_readonly_access!
+ can! :read_project
+ can! :read_board
+ can! :read_list
+ can! :read_wiki
+ can! :read_label
+ can! :read_milestone
+ can! :read_project_snippet
+ can! :read_project_member
+ can! :read_merge_request
+ can! :read_note
+ can! :read_pipeline
+ can! :read_commit_status
+ can! :read_container_image
+ can! :download_code
+ can! :download_wiki_code
+ can! :read_cycle_analytics
+
+ # NOTE: may be overridden by IssuePolicy
+ can! :read_issue
+ end
end
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index 57acccfafd9..3a96836917e 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy
can! :read_project_snippet if @subject.public?
return unless @user
- if @user && @subject.author == @user || @user.admin?
+ if @user && (@subject.author == @user || @user.admin?)
can! :read_project_snippet
can! :update_project_snippet
can! :admin_project_snippet
diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb
index de9a181db90..311ee9c96be 100644
--- a/app/serializers/base_serializer.rb
+++ b/app/serializers/base_serializer.rb
@@ -6,6 +6,7 @@ class BaseSerializer
def represent(resource, opts = {})
self.class.entity_class
.represent(resource, opts.merge(request: @request))
+ .as_json
end
def self.entity(entity_class)
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 5d15eb8d3d3..4c017960628 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
- expose :stoppable?
+ expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index 91955542f25..fe16a3784c4 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -1,3 +1,50 @@
class EnvironmentSerializer < BaseSerializer
+ Item = Struct.new(:name, :size, :latest)
+
entity EnvironmentEntity
+
+ def within_folders
+ tap { @itemize = true }
+ end
+
+ def with_pagination(request, response)
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
+ end
+
+ def itemized?
+ @itemize
+ end
+
+ def paginated?
+ @paginator.present?
+ end
+
+ def represent(resource, opts = {})
+ resource = @paginator.paginate(resource) if paginated?
+
+ if itemized?
+ itemize(resource).map do |item|
+ { name: item.name,
+ size: item.size,
+ latest: super(item.latest, opts) }
+ end
+ else
+ super(resource, opts)
+ end
+ end
+
+ private
+
+ def itemize(resource)
+ items = resource.group(:item_name).order('item_name ASC')
+ .pluck('COALESCE(environment_type, name) AS item_name',
+ 'COUNT(*) AS environments_count',
+ 'MAX(id) AS last_environment_id')
+
+ environments = resource.where(id: items.map(&:last)).index_by(&:id)
+
+ items.map do |name, size, id|
+ Item.new(name, size, environments[id])
+ end
+ end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index cfa86cc2553..2bc6cf3266e 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -1,40 +1,25 @@
class PipelineSerializer < BaseSerializer
- entity PipelineEntity
class InvalidResourceError < StandardError; end
- include API::Helpers::Pagination
- Struct.new('Pagination', :request, :response)
-
- def represent(resource, opts = {})
- if paginated?
- raise InvalidResourceError unless resource.respond_to?(:page)
- super(paginate(resource.includes(project: :namespace)), opts)
- else
- super(resource, opts)
- end
- end
-
- def paginated?
- defined?(@pagination)
- end
+ entity PipelineEntity
def with_pagination(request, response)
- tap { @pagination = Struct::Pagination.new(request, response) }
+ tap { @paginator = Gitlab::Serializer::Pagination.new(request, response) }
end
- private
-
- # Methods needed by `API::Helpers::Pagination`
- #
- def params
- @pagination.request.query_parameters
+ def paginated?
+ @paginator.present?
end
- def request
- @pagination.request
- end
+ def represent(resource, opts = {})
+ if resource.is_a?(ActiveRecord::Relation)
+ resource = resource.includes(project: :namespace)
+ end
- def header(header, value)
- @pagination.response.headers[header] = value
+ if paginated?
+ super(@paginator.paginate(resource), opts)
+ else
+ super(resource, opts)
+ end
end
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index c00c5aebf57..5cb7a86a5ee 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -61,7 +61,7 @@ module Auth
end
def process_repository_access(type, name, actions)
- requested_project = Project.find_with_namespace(name)
+ requested_project = Project.find_by_full_path(name)
return unless requested_project
actions = actions.select do |action|
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 9bdd7b6f0cf..f6275a63109 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -12,7 +12,6 @@ module Boards
def create_board!
board = project.boards.create
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
board
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index fd4a462c7b2..8a94c54b6ab 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,8 +3,8 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless list.movable?
- issues = with_list_label(issues) if list.movable?
+ issues = without_board_labels(issues) unless movable_list?
+ issues = with_list_label(issues) if movable_list?
issues
end
@@ -15,7 +15,13 @@ module Boards
end
def list
- @list ||= board.lists.find(params[:id])
+ return @list if defined?(@list)
+
+ @list = board.lists.find(params[:id]) if params.key?(:id)
+ end
+
+ def movable_list?
+ @movable_list ||= list.present? && list.movable?
end
def filter_params
@@ -40,7 +46,7 @@ module Boards
end
def set_state
- params[:state] = list.done? ? 'closed' : 'opened'
+ params[:state] = list && list.done? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb
index cf590459cb2..42c72aba7dd 100644
--- a/app/services/ci/stop_environments_service.rb
+++ b/app/services/ci/stop_environments_service.rb
@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
- next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
- environment.stop!(current_user)
+ environment.stop_with_action!(current_user)
end
end
@@ -22,8 +21,8 @@ module Ci
end
def environments
- @environments ||= project
- .environments_recently_updated_on_branch(@ref)
+ @environments ||=
+ EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end
end
end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 4d410f66c55..25e22f14e60 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -4,7 +4,8 @@ module Commits
class ChangeError < StandardError; end
def execute
- @source_project = params[:source_project] || @project
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
@target_branch = params[:target_branch]
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
@@ -25,13 +26,28 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
- into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch
- tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch)
+ if @create_merge_request
+ into = @commit.public_send("#{action}_branch_name")
+ tree_branch = @start_branch
+ else
+ into = tree_branch = @target_branch
+ end
+
+ tree_id = repository.public_send(
+ "check_#{action}_content", @commit, tree_branch)
if tree_id
- create_target_branch(into) if @create_merge_request
+ validate_target_branch(into) if @create_merge_request
+
+ repository.public_send(
+ action,
+ current_user,
+ @commit,
+ into,
+ tree_id,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
- repository.public_send(action, current_user, @commit, into, tree_id)
success
else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
@@ -50,12 +66,12 @@ module Commits
true
end
- def create_target_branch(new_branch)
+ def validate_target_branch(new_branch)
# Temporary branch exists and contains the change commit
- return success if repository.find_branch(new_branch)
+ return if repository.find_branch(new_branch)
- result = CreateBranchService.new(@project, current_user)
- .execute(new_branch, @target_branch, source_project: @source_project)
+ result = ValidateNewBranchService.new(@project, current_user)
+ .execute(new_branch)
if result[:status] == :error
raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 5e8fafca98c..ab4c02a97a0 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,23 +3,27 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch, straight: false)
- source_commit = source_project.commit(source_branch)
- return unless source_commit
+ attr_reader :start_project, :start_branch_name
- source_sha = source_commit.sha
+ def initialize(new_start_project, new_start_branch_name)
+ @start_project = new_start_project
+ @start_branch_name = new_start_branch_name
+ end
+ def execute(target_project, target_branch, straight: false)
# If compare with other project we need to fetch ref first
- unless target_project == source_project
- random_string = SecureRandom.hex
+ target_project.repository.with_repo_branch_commit(
+ start_project.repository,
+ start_branch_name) do |commit|
+ break unless commit
- target_project.repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{source_branch}",
- "refs/tmp/#{random_string}/head"
- )
+ compare(commit.sha, target_project, target_branch, straight)
end
+ end
+
+ private
+ def compare(source_sha, target_project, target_branch, straight)
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index e004a303496..77459d8779d 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,31 +1,11 @@
class CreateBranchService < BaseService
- def execute(branch_name, ref, source_project: @project)
- valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+ def execute(branch_name, ref)
+ result = ValidateNewBranchService.new(project, current_user)
+ .execute(branch_name)
- unless valid_branch
- return error('Branch name is invalid')
- end
-
- repository = project.repository
- existing_branch = repository.find_branch(branch_name)
-
- if existing_branch
- return error('Branch already exists')
- end
-
- new_branch = if source_project != @project
- repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{ref}",
- "refs/heads/#{branch_name}"
- )
-
- repository.after_create_branch
+ return result if result[:status] == :error
- repository.find_branch(branch_name)
- else
- repository.add_branch(current_user, branch_name, ref)
- end
+ new_branch = repository.add_branch(current_user, branch_name, ref)
if new_branch
success(new_branch)
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
index 95cc9baf406..14f5ba064ff 100644
--- a/app/services/create_snippet_service.rb
+++ b/app/services/create_snippet_service.rb
@@ -1,5 +1,8 @@
class CreateSnippetService < BaseService
def execute
+ request = params.delete(:request)
+ api = params.delete(:api)
+
snippet = if project
project.snippets.build(params)
else
@@ -12,8 +15,12 @@ class CreateSnippetService < BaseService
end
snippet.author = current_user
+ snippet.spam = SpamService.new(snippet, request).check(api)
+
+ if snippet.save
+ UserAgentDetailService.new(snippet, request).create
+ end
- snippet.save
snippet
end
end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index fe9353afeb8..6c75d1f04ff 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -4,7 +4,7 @@ class CreateTagService < BaseService
return error('Tag name invalid') unless valid_tag
repository = project.repository
- message.strip! if message
+ message&.strip!
new_tag = nil
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index a44dee14a0f..eb726cb04b1 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -7,9 +7,9 @@ class DeleteTagService < BaseService
return error('No such tag', 404)
end
- if repository.rm_tag(tag_name)
+ if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
- release.destroy if release
+ release&.destroy
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
deleted file mode 100644
index eaff88d6463..00000000000
--- a/app/services/delete_user_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-class DeleteUserService
- attr_accessor :current_user
-
- def initialize(current_user)
- @current_user = current_user
- end
-
- def execute(user, options = {})
- if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
- user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
- return user
- end
-
- user.solo_owned_groups.each do |group|
- DestroyGroupService.new(group, current_user).execute
- end
-
- user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
- end
-
- # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- namespace = user.namespace
- user_data = user.destroy
- namespace.really_destroy!
-
- user_data
- end
-end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
deleted file mode 100644
index 2316c57bf1e..00000000000
--- a/app/services/destroy_group_service.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-class DestroyGroupService
- attr_accessor :group, :current_user
-
- def initialize(group, user)
- @group, @current_user = group, user
- end
-
- def async_execute
- # 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
- group.projects.each do |project|
- # Execute the destruction of the models immediately to ensure atomic cleanup.
- # Skip repository removal because we remove directory with namespace
- # that contain all these repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
- end
-
- group.children.each do |group|
- DestroyGroupService.new(group, current_user).async_execute
- end
-
- group.really_destroy!
- end
-end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 9bd4bd464f7..0a25f56d24c 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -3,9 +3,9 @@ module Files
class ValidationError < StandardError; end
def execute
- @source_project = params[:source_project] || @project
- @source_branch = params[:source_branch]
- @target_branch = params[:target_branch]
+ @start_project = params[:start_project] || @project
+ @start_branch = params[:start_branch]
+ @target_branch = params[:target_branch]
@commit_message = params[:commit_message]
@file_path = params[:file_path]
@@ -22,10 +22,8 @@ module Files
# Validate parameters
validate
- # Create new branch if it different from source_branch
- if different_branch?
- create_target_branch
- end
+ # Create new branch if it different from start_branch
+ validate_target_branch if different_branch?
result = commit
if result
@@ -40,7 +38,7 @@ module Files
private
def different_branch?
- @source_branch != @target_branch || @source_project != @project
+ @start_branch != @target_branch || @start_project != @project
end
def file_has_changed?
@@ -61,22 +59,23 @@ module Files
end
unless project.empty_repo?
- unless @source_project.repository.branch_names.include?(@source_branch)
+ unless @start_project.repository.branch_exists?(@start_branch)
raise_error('You can only create or edit files when you are on a branch')
end
if different_branch?
- if repository.branch_names.include?(@target_branch)
+ if repository.branch_exists?(@target_branch)
raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes')
end
end
end
end
- def create_target_branch
- result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project)
+ def validate_target_branch
+ result = ValidateNewBranchService.new(project, current_user).
+ execute(@target_branch)
- unless result[:status] == :success
+ if result[:status] == :error
raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index e5b4d60e467..858de5f0538 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -1,7 +1,15 @@
module Files
class CreateDirService < Files::BaseService
def commit
- repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
+ repository.commit_dir(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
def validate
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index b23576b9a28..88dd7bbaedb 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,7 +1,17 @@
module Files
class CreateService < Files::BaseService
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name)
+ repository.commit_file(
+ current_user,
+ @file_path,
+ @file_content,
+ message: @commit_message,
+ branch_name: @target_branch,
+ update: false,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
def validate
@@ -24,7 +34,7 @@ module Files
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
- blob = repository.blob_at_branch(@source_branch, @file_path)
+ blob = repository.blob_at_branch(@start_branch, @file_path)
if blob
raise_error('Your changes could not be committed because a file with the same name already exists')
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
deleted file mode 100644
index 4f7e7a5baaa..00000000000
--- a/app/services/files/delete_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Files
- class DeleteService < Files::BaseService
- def commit
- repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name)
- end
- end
-end
diff --git a/app/services/files/destroy_service.rb b/app/services/files/destroy_service.rb
new file mode 100644
index 00000000000..c3be806a42d
--- /dev/null
+++ b/app/services/files/destroy_service.rb
@@ -0,0 +1,15 @@
+module Files
+ class DestroyService < Files::BaseService
+ def commit
+ repository.remove_file(
+ current_user,
+ @file_path,
+ message: @commit_message,
+ branch_name: @target_branch,
+ author_email: @author_email,
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
+ end
+ end
+end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index 54446e90007..6ba868df04d 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -5,11 +5,13 @@ module Files
def commit
repository.multi_action(
user: current_user,
- branch: @target_branch,
message: @commit_message,
+ branch_name: @target_branch,
actions: params[:actions],
author_email: @author_email,
- author_name: @author_name
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch
)
end
@@ -61,7 +63,7 @@ module Files
end
def last_commit
- Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path)
end
def regex_check(file)
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 47a18e3e132..a71fe61a4b6 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -4,11 +4,13 @@ module Files
def commit
repository.update_file(current_user, @file_path, @file_content,
- branch: @target_branch,
- previous_path: @previous_path,
message: @commit_message,
+ branch_name: @target_branch,
+ previous_path: @previous_path,
author_email: @author_email,
- author_name: @author_name)
+ author_name: @author_name,
+ start_project: @start_project,
+ start_branch_name: @start_branch)
end
private
@@ -23,7 +25,7 @@ module Files
def last_commit
@last_commit ||= Gitlab::Git::Commit.
- last_for_path(@source_project.repository, @source_branch, @file_path)
+ last_for_path(@start_project.repository, @start_branch, @file_path)
end
end
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
index 6cd3908d43a..d222d1e63aa 100644
--- a/app/services/git_hooks_service.rb
+++ b/app/services/git_hooks_service.rb
@@ -18,9 +18,9 @@ class GitHooksService
end
end
- yield self
-
- run_hook('post-receive')
+ yield(self).tap do
+ run_hook('post-receive')
+ end
end
private
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
new file mode 100644
index 00000000000..27bcc047601
--- /dev/null
+++ b/app/services/git_operation_service.rb
@@ -0,0 +1,179 @@
+class GitOperationService
+ attr_reader :user, :repository
+
+ def initialize(new_user, new_repository)
+ @user = new_user
+ @repository = new_repository
+ end
+
+ def add_branch(branch_name, newrev)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def rm_branch(branch)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
+ oldrev = branch.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def add_tag(tag_name, newrev, options = {})
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ with_hooks(ref, newrev, oldrev) do |service|
+ # We want to pass the OID of the tag object to the hooks. For an
+ # annotated tag we don't know that OID until after the tag object
+ # (raw_tag) is created in the repository. That is why we have to
+ # update the value after creating the tag object. Only the
+ # "post-receive" hook will receive the correct value in this case.
+ raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
+ service.newrev = raw_tag.target_id
+ end
+ end
+
+ def rm_tag(tag)
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
+ oldrev = tag.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev) do
+ repository.rugged.tags.delete(tag_name)
+ end
+ end
+
+ # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
+ # it would be created from `start_branch_name`.
+ # If `start_project` is passed, and the branch doesn't exist,
+ # it would try to find the commits from it instead of current repository.
+ def with_branch(
+ branch_name,
+ start_branch_name: nil,
+ start_project: repository.project,
+ &block)
+
+ check_with_branch_arguments!(
+ branch_name, start_branch_name, start_project)
+
+ update_branch_with_hooks(branch_name) do
+ repository.with_repo_branch_commit(
+ start_project.repository,
+ start_branch_name || branch_name,
+ &block)
+ end
+ end
+
+ private
+
+ def update_branch_with_hooks(branch_name)
+ update_autocrlf_option
+
+ was_empty = repository.empty?
+
+ # Make commit
+ newrev = yield
+
+ unless newrev
+ raise Repository::CommitError.new('Failed to create commit')
+ end
+
+ branch = repository.find_branch(branch_name)
+ oldrev = find_oldrev_from_branch(newrev, branch)
+
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ update_ref_in_hooks(ref, newrev, oldrev)
+
+ # If repo was empty expire cache
+ repository.after_create if was_empty
+ repository.after_create_branch if
+ was_empty || Gitlab::Git.blank_ref?(oldrev)
+
+ newrev
+ end
+
+ def find_oldrev_from_branch(newrev, branch)
+ return Gitlab::Git::BLANK_SHA unless branch
+
+ oldrev = branch.target
+
+ if oldrev == repository.rugged.merge_base(newrev, branch.target)
+ oldrev
+ else
+ raise Repository::CommitError.new('Branch diverged')
+ end
+ end
+
+ def update_ref_in_hooks(ref, newrev, oldrev)
+ with_hooks(ref, newrev, oldrev) do
+ update_ref(ref, newrev, oldrev)
+ end
+ end
+
+ def with_hooks(ref, newrev, oldrev)
+ GitHooksService.new.execute(
+ user,
+ repository.path_to_repo,
+ oldrev,
+ newrev,
+ ref) do |service|
+
+ yield(service)
+ end
+ end
+
+ def update_ref(ref, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # 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[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(
+ command,
+ repository.path_to_repo) do |stdin|
+ stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ unless status.zero?
+ raise Repository::CommitError.new(
+ "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
+ " Please refresh and try again.")
+ end
+ end
+
+ def update_autocrlf_option
+ if repository.raw_repository.autocrlf != :input
+ repository.raw_repository.autocrlf = :input
+ end
+ end
+
+ def check_with_branch_arguments!(
+ branch_name, start_branch_name, start_project)
+ return if repository.branch_exists?(branch_name)
+
+ if repository.project != start_project
+ unless start_branch_name
+ raise ArgumentError,
+ 'Should also pass :start_branch_name if' +
+ ' :start_project is different from current project'
+ end
+
+ unless start_project.repository.branch_exists?(start_branch_name)
+ raise ArgumentError,
+ "Cannot find branch #{branch_name} nor" \
+ " #{start_branch_name} from" \
+ " #{start_project.path_with_namespace}"
+ end
+ elsif start_branch_name
+ unless repository.branch_exists?(start_branch_name)
+ raise ArgumentError,
+ "Cannot find branch #{branch_name} nor" \
+ " #{start_branch_name} from" \
+ " #{repository.project.path_with_namespace}"
+ end
+ end
+ end
+end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
new file mode 100644
index 00000000000..7f2d28086f5
--- /dev/null
+++ b/app/services/groups/destroy_service.rb
@@ -0,0 +1,25 @@
+module Groups
+ class DestroyService < Groups::BaseService
+ def async_execute
+ # 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
+ group.projects.each do |project|
+ # Execute the destruction of the models immediately to ensure atomic cleanup.
+ # Skip repository removal because we remove directory with namespace
+ # that contain all these repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ end
+
+ group.children.each do |group|
+ DestroyService.new(group, current_user).async_execute
+ end
+
+ group.really_destroy!
+ end
+ end
+end
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index a63982f60c8..7cd927d8005 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -44,7 +44,15 @@ module Issues
end
def issue_params
- @issue_params ||= issue_params_with_info_from_merge_request.merge(params.slice(:title, :description))
+ @issue_params ||= issue_params_with_info_from_merge_request.merge(whitelisted_issue_params)
+ end
+
+ def whitelisted_issue_params
+ if can?(current_user, :admin_issue, project)
+ params.slice(:title, :description, :milestone_id)
+ else
+ params.slice(:title, :description)
+ end
end
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index d2eb46ac41b..961605a1005 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,6 +3,8 @@ module Issues
def execute
@request = params.delete(:request)
@api = params.delete(:api)
+ @recaptcha_verified = params.delete(:recaptcha_verified)
+ @spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
@@ -11,7 +13,13 @@ module Issues
end
def before_create(issuable)
- issuable.spam = spam_service.check(@api)
+ if @recaptcha_verified
+ spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
+ spam_log&.update!(recaptcha_verified: true)
+ else
+ issuable.spam = spam_service.check(@api)
+ issuable.spam_log = spam_service.spam_log
+ end
end
def after_create(issuable)
@@ -35,7 +43,7 @@ module Issues
private
def spam_service
- SpamService.new(@issue, @request)
+ @spam_service ||= SpamService.new(@issue, @request)
end
def user_agent_detail_service
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
new file mode 100644
index 00000000000..76d0ba67b07
--- /dev/null
+++ b/app/services/labels/promote_service.rb
@@ -0,0 +1,71 @@
+module Labels
+ class PromoteService < BaseService
+ BATCH_SIZE = 1000
+
+ def execute(label)
+ return unless project.group &&
+ label.is_a?(ProjectLabel)
+
+ Label.transaction do
+ new_label = clone_label_to_group_label(label)
+
+ label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids|
+ update_issuables(new_label, batched_ids)
+ update_issue_board_lists(new_label, batched_ids)
+ update_priorities(new_label, batched_ids)
+ # Order is important, project labels need to be last
+ update_project_labels(batched_ids)
+ end
+
+ # We skipped validations during creation. Let's run them now, after deleting conflicting labels
+ raise ActiveRecord::RecordInvalid.new(new_label) unless new_label.valid?
+ new_label
+ end
+ end
+
+ private
+
+ def label_ids_for_merge(new_label)
+ LabelsFinder.
+ new(current_user, title: new_label.title, group_id: project.group.id).
+ execute(skip_authorization: true).
+ where.not(id: new_label).
+ select(:id) # Can't use pluck() to avoid object-creation because of the batching
+ end
+
+ def update_issuables(new_label, label_ids)
+ LabelLink.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_issue_board_lists(new_label, label_ids)
+ List.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_priorities(new_label, label_ids)
+ LabelPriority.
+ where(label: label_ids).
+ update_all(label_id: new_label)
+ end
+
+ def update_project_labels(label_ids)
+ Label.where(id: label_ids).delete_all
+ end
+
+ def clone_label_to_group_label(label)
+ params = label.attributes.slice('title', 'description', 'color')
+ # Since the title of the new label has to be the same as the previous labels
+ # and we're merging old labels in batches we'll skip validation to omit 2-step
+ # merge process and do it in one batch
+ # We'll be forcing validation at the end of the transaction to ensure everything
+ # was merged correctly
+ new_label = GroupLabel.new(params.merge(group: project.group))
+ new_label.save(validate: false)
+
+ new_label
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 6a7393a9921..f4d52e3ebbd 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -1,62 +1,89 @@
module MergeRequests
class BuildService < MergeRequests::BaseService
def execute
- merge_request = MergeRequest.new(params)
-
- # Set MR attributes
- merge_request.can_be_created = true
+ self.merge_request = MergeRequest.new(params)
+ merge_request.can_be_created = true
merge_request.compare_commits = []
- merge_request.source_project = project unless merge_request.source_project
+ merge_request.source_project = find_source_project
+ merge_request.target_project = find_target_project
+ merge_request.target_branch = find_target_branch
- merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project)
+ if branches_specified? && branches_valid?
+ compare_branches
+ assign_title_and_description
+ else
+ merge_request.can_be_created = false
+ end
- merge_request.target_project ||= (project.forked_from_project || project)
- merge_request.target_branch ||= merge_request.target_project.default_branch
+ merge_request
+ end
- messages = validate_branches(merge_request)
- return build_failed(merge_request, messages) unless messages.empty?
+ private
- compare = CompareService.new.execute(
- merge_request.source_project,
- merge_request.source_branch,
- merge_request.target_project,
- merge_request.target_branch,
- )
+ attr_accessor :merge_request
- merge_request.compare_commits = compare.commits
- merge_request.compare = compare
+ delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
- set_title_and_description(merge_request)
+ def find_source_project
+ source_project || project
end
- private
+ def find_target_project
+ return target_project if target_project.present? && can?(current_user, :read_project, target_project)
+ project.forked_from_project || project
+ end
- def validate_branches(merge_request)
- messages = []
+ def find_target_branch
+ target_branch || target_project.default_branch
+ end
- if merge_request.target_branch.blank? || merge_request.source_branch.blank?
- messages <<
- if params[:source_branch] || params[:target_branch]
- "You must select source and target branch"
- end
- end
+ def branches_specified?
+ params[:source_branch] && params[:target_branch]
+ end
- if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
+ def branches_valid?
+ validate_branches
+ errors.blank?
+ end
- messages << 'You must select different branches'
- end
+ def compare_branches
+ compare = CompareService.new(
+ source_project,
+ source_branch
+ ).execute(
+ target_project,
+ target_branch
+ )
- # See if source and target branches exist
- 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
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
+ end
- 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
+ def validate_branches
+ add_error('You must select source and target branch') unless branches_present?
+ add_error('You must select different branches') if same_source_and_target?
+ add_error("Source branch \"#{source_branch}\" does not exist") unless source_branch_exists?
+ add_error("Target branch \"#{target_branch}\" does not exist") unless target_branch_exists?
+ end
+
+ def add_error(message)
+ errors.add(:base, message)
+ end
+
+ def branches_present?
+ target_branch.present? && source_branch.present?
+ end
+
+ def same_source_and_target?
+ source_project == target_project && target_branch == source_branch
+ end
- messages
+ def source_branch_exists?
+ source_branch.blank? || source_project.commit(source_branch)
+ end
+
+ def target_branch_exists?
+ target_branch.blank? || target_project.commit(target_branch)
end
# When your branch name starts with an iid followed by a dash this pattern will be
@@ -71,17 +98,17 @@ module MergeRequests
# - Setting the title as 'Resolves "Emoji don't show up in commit title"' if there is
# more than one commit in the MR
#
- def set_title_and_description(merge_request)
- if match = merge_request.source_branch.match(/\A(\d+)-/)
+ def assign_title_and_description
+ if match = source_branch.match(/\A(\d+)-/)
iid = match[1]
end
- commits = merge_request.compare_commits
+ commits = compare_commits
if commits && commits.count == 1
commit = commits.first
merge_request.title = commit.title
merge_request.description ||= commit.description.try(:strip)
- elsif iid && issue = merge_request.target_project.get_issue(iid, current_user)
+ elsif iid && issue = target_project.get_issue(iid, current_user)
case issue
when Issue
merge_request.title = "Resolve \"#{issue.title}\""
@@ -89,31 +116,20 @@ module MergeRequests
merge_request.title = "Resolve #{issue.title}"
end
else
- merge_request.title = merge_request.source_branch.titleize.humanize
+ merge_request.title = source_branch.titleize.humanize
end
if iid
closes_issue = "Closes ##{iid}"
- if merge_request.description.present?
+ if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
end
- merge_request.title = merge_request.wip_title if commits.empty?
-
- merge_request
- end
-
- def build_failed(merge_request, messages)
- messages.compact.each do |message|
- merge_request.errors.add(:base, message)
- end
- merge_request.compare_commits = []
- merge_request.can_be_created = false
- merge_request
+ merge_request.title = wip_title if commits.empty?
end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index ab9056a3250..5ca6fec962d 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,13 +6,17 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
- attr_reader :merge_request
+ attr_reader :merge_request, :source
def execute(merge_request)
@merge_request = merge_request
return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
+ @source = find_merge_source
+
+ return log_merge_error('No source for merge', true) unless @source
+
merge_request.in_locked_state do
if commit
after_merge
@@ -34,7 +38,7 @@ module MergeRequests
committer: committer
}
- commit_id = repository.merge(current_user, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, options)
if commit_id
merge_request.update(merge_commit_sha: commit_id)
@@ -73,9 +77,11 @@ module MergeRequests
end
def merge_request_info
- project = merge_request.project
+ merge_request.to_reference(full: true)
+ end
- "#{project.to_reference}#{merge_request.to_reference}"
+ def find_merge_source
+ merge_request.diff_head_sha
end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b4bfb0e5e8c..581d18032e6 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -144,7 +144,11 @@ module MergeRequests
return unless @commits.present?
merge_requests_for_source_branch.each do |merge_request|
- wip_commit = @commits.detect(&:work_in_progress?)
+ commit_shas = merge_request.commits_sha
+
+ wip_commit = @commits.detect do |commit|
+ commit.work_in_progress? && commit_shas.include?(commit.sha)
+ end
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
diff --git a/app/services/notes/delete_service.rb b/app/services/notes/destroy_service.rb
index a673e8e9dde..b819bd17039 100644
--- a/app/services/notes/delete_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,5 +1,5 @@
module Notes
- class DeleteService < BaseService
+ class DestroyService < BaseService
def execute(note)
note.destroy
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f74e6cac174..b2cc39763f3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -365,7 +365,7 @@ class NotificationService
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users)
+ users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
@@ -415,8 +415,8 @@ class NotificationService
end
# Build a list of users based on group notification settings
- def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(project, :watch)
+ def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
+ uids = notification_settings_for(group, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -473,7 +473,7 @@ class NotificationService
setting = user.notification_settings_for(project)
- if !setting && project.group
+ if project.group && (setting.nil? || setting.global?)
setting = user.notification_settings_for(project.group)
end
diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb
new file mode 100644
index 00000000000..446eeb34d3b
--- /dev/null
+++ b/app/services/pages_service.rb
@@ -0,0 +1,15 @@
+class PagesService
+ attr_reader :data
+
+ def initialize(data)
+ @data = data
+ end
+
+ def execute
+ return unless Settings.pages.enabled
+ return unless data[:build_name] == 'pages'
+ return unless data[:build_status] == 'success'
+
+ PagesWorker.perform_async(:deploy, data[:build_id])
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index c7cce0c55b9..6dc3d8c2416 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -97,7 +97,7 @@ module Projects
@project.team << [current_user, :master, current_user]
end
- @project.group.refresh_members_authorized_projects if @project.group
+ @project.group&.refresh_members_authorized_projects
end
def skip_wiki?
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 06252c7b625..535da706159 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
end
def uploads_saver
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 34ec575e808..484700c8c29 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -25,9 +25,10 @@ module Projects
end
def transfer(project, new_namespace)
+ old_namespace = project.namespace
+
Project.transaction do
old_path = project.path_with_namespace
- old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
@@ -64,11 +65,17 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ # Move pages
+ Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
- true
end
+
+ refresh_permissions(old_namespace, new_namespace)
+
+ true
end
def allowed_transfer?(current_user, project, namespace)
@@ -77,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
+
+ def refresh_permissions(old_namespace, new_namespace)
+ # This ensures we only schedule 1 job for every user that has access to
+ # the namespaces.
+ user_ids = old_namespace.user_ids_for_project_authorizations |
+ new_namespace.user_ids_for_project_authorizations
+
+ UserProjectAccessChangedService.new(user_ids).execute
+ end
end
end
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
new file mode 100644
index 00000000000..eb4809afa85
--- /dev/null
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -0,0 +1,69 @@
+module Projects
+ class UpdatePagesConfigurationService < BaseService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ update_file(pages_config_file, pages_config.to_json)
+ reload_daemon
+ success
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def pages_config
+ {
+ domains: pages_domains_config
+ }
+ end
+
+ def pages_domains_config
+ project.pages_domains.map do |domain|
+ {
+ domain: domain.domain,
+ certificate: domain.certificate,
+ key: domain.key,
+ }
+ end
+ end
+
+ def reload_daemon
+ # GitLab Pages daemon constantly watches for modification time of `pages.path`
+ # It reloads configuration when `pages.path` is modified
+ update_file(pages_update_file, SecureRandom.hex(64))
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def pages_config_file
+ File.join(pages_path, 'config.json')
+ end
+
+ def pages_update_file
+ File.join(::Settings.pages.path, '.update')
+ end
+
+ def update_file(file, data)
+ unless data
+ FileUtils.remove(file, force: true)
+ return
+ end
+
+ temp_file = "#{file}.#{SecureRandom.hex(16)}"
+ File.open(temp_file, 'w') do |f|
+ f.write(data)
+ end
+ FileUtils.move(temp_file, file, force: true)
+ ensure
+ # In case if the updating fails
+ FileUtils.remove(temp_file, force: true)
+ end
+ end
+end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
new file mode 100644
index 00000000000..f5f9ee88912
--- /dev/null
+++ b/app/services/projects/update_pages_service.rb
@@ -0,0 +1,164 @@
+module Projects
+ class UpdatePagesService < BaseService
+ BLOCK_SIZE = 32.kilobytes
+ MAX_SIZE = 1.terabyte
+ SITE_PATH = 'public/'
+
+ attr_reader :build
+
+ def initialize(project, build)
+ @project, @build = project, build
+ end
+
+ def execute
+ # Create status notifying the deployment of pages
+ @status = create_status
+ @status.enqueue!
+ @status.run!
+
+ raise 'missing pages artifacts' unless build.artifacts_file?
+ raise 'pages are outdated' unless latest?
+
+ # Create temporary directory in which we will extract the artifacts
+ FileUtils.mkdir_p(tmp_path)
+ Dir.mktmpdir(nil, tmp_path) do |archive_path|
+ extract_archive!(archive_path)
+
+ # Check if we did extract public directory
+ archive_public_path = File.join(archive_path, 'public')
+ raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise 'pages are outdated' unless latest?
+
+ deploy_page!(archive_public_path)
+ success
+ end
+ rescue => e
+ error(e.message)
+ end
+
+ private
+
+ def success
+ @status.success
+ super
+ end
+
+ def error(message, http_status = nil)
+ @status.allow_failure = !latest?
+ @status.description = message
+ @status.drop
+ super
+ end
+
+ def create_status
+ GenericCommitStatus.new(
+ project: project,
+ pipeline: build.pipeline,
+ user: build.user,
+ ref: build.ref,
+ stage: 'deploy',
+ name: 'pages:deploy'
+ )
+ end
+
+ def extract_archive!(temp_path)
+ if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz')
+ extract_tar_archive!(temp_path)
+ elsif artifacts.ends_with?('.zip')
+ extract_zip_archive!(temp_path)
+ else
+ raise 'unsupported artifacts format'
+ end
+ end
+
+ def extract_tar_archive!(temp_path)
+ results = Open3.pipeline(%W(gunzip -c #{artifacts}),
+ %W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
+ %W(tar -x -C #{temp_path} #{SITE_PATH}),
+ err: '/dev/null')
+ raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ end
+
+ def extract_zip_archive!(temp_path)
+ raise 'missing artifacts metadata' unless build.artifacts_metadata?
+
+ # Calculate page size after extract
+ public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
+
+ if public_entry.total_size > max_size
+ raise "artifacts for pages are too large: #{public_entry.total_size}"
+ end
+
+ # Requires UnZip at least 6.00 Info-ZIP.
+ # -n never overwrite existing files
+ # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
+ site_path = File.join(SITE_PATH, '*')
+ unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path}))
+ raise 'pages failed to extract'
+ end
+ end
+
+ def deploy_page!(archive_public_path)
+ # Do atomic move of pages
+ # Move and removal may not be atomic, but they are significantly faster then extracting and removal
+ # 1. We move deployed public to previous public path (file removal is slow)
+ # 2. We move temporary public to be deployed public
+ # 3. We remove previous public path
+ FileUtils.mkdir_p(pages_path)
+ begin
+ FileUtils.move(public_path, previous_public_path)
+ rescue
+ end
+ FileUtils.move(archive_public_path, public_path)
+ ensure
+ FileUtils.rm_r(previous_public_path, force: true)
+ end
+
+ def latest?
+ # check if sha for the ref is still the most recent one
+ # this helps in case when multiple deployments happens
+ sha == latest_sha
+ end
+
+ def blocks
+ # Calculate dd parameters: we limit the size of pages
+ 1 + max_size / BLOCK_SIZE
+ end
+
+ def max_size
+ current_application_settings.max_pages_size.megabytes || MAX_SIZE
+ end
+
+ def tmp_path
+ @tmp_path ||= File.join(::Settings.pages.path, 'tmp')
+ end
+
+ def pages_path
+ @pages_path ||= project.pages_path
+ end
+
+ def public_path
+ @public_path ||= File.join(pages_path, 'public')
+ end
+
+ def previous_public_path
+ @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}")
+ end
+
+ def ref
+ build.ref
+ end
+
+ def artifacts
+ build.artifacts_file.path
+ end
+
+ def latest_sha
+ project.commit(build.ref).try(:sha).to_s
+ end
+
+ def sha
+ build.sha
+ end
+ end
+end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index aa9837038a6..781cd13b44b 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -9,7 +9,10 @@ module Search
def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
- projects = projects.in_namespace(group.id) if group
+
+ if group
+ projects = projects.inside_path(group.full_path)
+ end
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 3566a8ba92f..3e0a85cf059 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -304,6 +304,18 @@ module SlashCommands
params '@user'
command :cc
+ desc 'Defines target branch for MR'
+ params '<Local branch name>'
+ condition do
+ issuable.respond_to?(:target_branch) &&
+ (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) ||
+ issuable.new_record?)
+ end
+ command :target_branch do |target_branch_param|
+ branch_name = target_branch_param.strip
+ @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name)
+ end
+
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 48903291799..024a7c19d33 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -1,5 +1,6 @@
class SpamService
attr_accessor :spammable, :request, :options
+ attr_reader :spam_log
def initialize(spammable, request = nil)
@spammable = spammable
@@ -63,7 +64,7 @@ class SpamService
end
def create_spam_log(api)
- SpamLog.create(
+ @spam_log = SpamLog.create!(
{
user_id: spammable_owner_id,
title: spammable.spam_title,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index a11bca00687..87ba72cf991 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -118,16 +118,18 @@ module SystemNoteService
#
# Example Note text:
#
- # "Changed estimate of this issue to 3d 5h"
+ # "removed time estimate"
+ #
+ # "changed time estimate to 3d 5h"
#
# Returns the created Note object
def change_time_estimate(noteable, project, author)
parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate)
body = if noteable.time_estimate == 0
- "Removed time estimate on this #{noteable.human_class_name}"
+ "removed time estimate"
else
- "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}"
+ "changed time estimate to #{parsed_time}"
end
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -142,7 +144,9 @@ module SystemNoteService
#
# Example Note text:
#
- # "Added 2h 30m of time spent on this issue"
+ # "removed time spent"
+ #
+ # "added 2h 30m of time spent"
#
# Returns the created Note object
@@ -150,11 +154,11 @@ module SystemNoteService
time_spent = noteable.time_spent
if time_spent == :reset
- body = "Removed time spent on this #{noteable.human_class_name}"
+ body = "removed time spent"
else
parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs)
- action = time_spent > 0 ? 'Added' : 'Subtracted'
- body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}"
+ action = time_spent > 0 ? 'added' : 'subtracted'
+ body = "#{action} #{parsed_time} of time spent"
end
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -221,7 +225,7 @@ module SystemNoteService
end
def discussion_continued_in_issue(discussion, project, author, issue)
- body = "Added #{issue.to_reference} to continue this discussion"
+ body = "created #{issue.to_reference} to continue this discussion"
note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
note_attributes[:type] = note_attributes.delete(:note_type)
@@ -260,7 +264,7 @@ module SystemNoteService
#
# Example Note text:
#
- # "made the issue confidential"
+ # "made the issue confidential"
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
@@ -381,6 +385,7 @@ module SystemNoteService
# Returns Boolean
def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
+ return true if noteable.is_a?(Issuable) && (noteable.try(:closed?) || noteable.try(:merged?))
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 1bd6ce416ab..8ab943f4639 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -243,6 +243,12 @@ class TodoService
end
def create_mention_todos(project, target, author, note = nil)
+ # Create Todos for directly addressed users
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
+ attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
+ create_todos(directly_addressed_users, attributes)
+
+ # Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
@@ -282,10 +288,18 @@ class TodoService
)
end
+ def filter_todo_users(users, project, target)
+ reject_users_without_access(users, project, target).uniq
+ end
+
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users(author)
- mentioned_users = reject_users_without_access(mentioned_users, project, target)
- mentioned_users.uniq
+ filter_todo_users(mentioned_users, project, target)
+ end
+
+ def filter_directly_addressed_users(project, target, author)
+ directly_addressed_users = target.directly_addressed_users(author)
+ filter_todo_users(directly_addressed_users, project, target)
end
def reject_users_without_access(users, project, target)
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
new file mode 100644
index 00000000000..2d11305be13
--- /dev/null
+++ b/app/services/users/destroy_service.rb
@@ -0,0 +1,33 @@
+module Users
+ class DestroyService
+ attr_accessor :current_user
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(user, options = {})
+ if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
+ user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
+ return user
+ end
+
+ user.solo_owned_groups.each do |group|
+ Groups::DestroyService.new(group, current_user).execute
+ end
+
+ user.personal_projects.each do |project|
+ # Skip repository removal because we remove directory with namespace
+ # that contain all this repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
+ end
+
+ # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+ namespace = user.namespace
+ user_data = user.destroy
+ namespace.really_destroy!
+
+ user_data
+ end
+ end
+end
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
new file mode 100644
index 00000000000..2f61be184ce
--- /dev/null
+++ b/app/services/validate_new_branch_service.rb
@@ -0,0 +1,22 @@
+require_relative 'base_service'
+
+class ValidateNewBranchService < BaseService
+ def execute(branch_name)
+ valid_branch = Gitlab::GitRefValidator.validate(branch_name)
+
+ unless valid_branch
+ return error('Branch name is invalid')
+ end
+
+ repository = project.repository
+ existing_branch = repository.find_branch(branch_name)
+
+ if existing_branch
+ return error('Branch already exists')
+ end
+
+ success
+ rescue GitHooksService::PreReceiveError => ex
+ error(ex.message)
+ end
+end
diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb
new file mode 100644
index 00000000000..098b16017d2
--- /dev/null
+++ b/app/validators/certificate_key_validator.rb
@@ -0,0 +1,25 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate_key: true
+# end
+#
+class CertificateKeyValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_private_key_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM private key")
+ end
+ end
+
+ private
+
+ def valid_private_key_pem?(value)
+ return false unless value
+ pkey = OpenSSL::PKey::RSA.new(value)
+ pkey.private?
+ rescue OpenSSL::PKey::PKeyError
+ false
+ end
+end
diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb
new file mode 100644
index 00000000000..e3d18097f71
--- /dev/null
+++ b/app/validators/certificate_validator.rb
@@ -0,0 +1,24 @@
+# UrlValidator
+#
+# Custom validator for private keys.
+#
+# class Project < ActiveRecord::Base
+# validates :certificate_key, certificate: true
+# end
+#
+class CertificateValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_certificate_pem?(value)
+ record.errors.add(attribute, "must be a valid PEM certificate")
+ end
+ end
+
+ private
+
+ def valid_certificate_pem?(value)
+ return false unless value
+ OpenSSL::X509::Certificate.new(value).present?
+ rescue OpenSSL::X509::CertificateError
+ false
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 558bbe07b16..816035ec442 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -187,6 +187,14 @@
.help-block Markdown enabled
%fieldset
+ %legend Pages
+ .form-group
+ = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_pages_size, class: 'form-control'
+ .help-block Zero for unlimited
+
+ %fieldset
%legend Continuous Integration
.form-group
.col-sm-offset-2.col-sm-10
@@ -204,7 +212,7 @@
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
.help-block
- Set the maximum file size each build's artifacts can have
+ Set the maximum file size each jobs's artifacts can have
= link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
- if Gitlab.config.registry.enabled
@@ -509,5 +517,15 @@
.help-block
Number of Git pushes after which 'git gc' is run.
+ %fieldset
+ %legend Web terminal
+ .form-group
+ = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :terminal_max_session_time, class: 'form-control'
+ .help-block
+ Maximum time for web terminal websocket connection (in seconds).
+ Set to 0 for unlimited time.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 5e3f105d41f..66d633119c2 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -12,7 +12,7 @@
= link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
.row-content-block.second-block
- #{(@scope || 'all').capitalize} builds
+ #{(@scope || 'all').capitalize} jobs
%ul.content-list.builds-content-list.admin-builds-table
= render "projects/builds/table", builds: @builds, admin: true
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b5f96363230..7893c1dee97 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,9 +20,9 @@
%span
Groups
= nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
+ = link_to admin_builds_path, title: 'Jobs' do
%span
- Builds
+ Jobs
= nav_link path: ['runners#index', 'runners#show'] do
= link_to admin_runners_path, title: 'Runners' do
%span
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 0a954c20fcd..13d00dd1fcb 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -18,7 +18,7 @@
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
id: klass::file_name_noext }
.file-holder#README
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file
= klass::file_name
.pull-right
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 2e6f03fcde0..cf8d438670b 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -27,7 +27,7 @@
= icon("search", class: "search-icon")
.dropdown
- - toggle_text = 'Search for Namespace'
+ - toggle_text = 'Namespace'
- if params[:namespace_id].present?
- namespace = Namespace.find(params[:namespace_id])
- toggle_text = "#{namespace.kind}: #{namespace.path}"
@@ -37,8 +37,10 @@
= dropdown_filter("Search for Namespace")
= dropdown_content
= dropdown_loading
-
- = button_tag "Search", class: "btn btn-primary btn-search"
+ = render 'shared/projects/dropdown'
+ = link_to new_project_path, class: 'btn btn-new' do
+ New Project
+ = button_tag "Search", class: "btn btn-primary btn-search hide"
%ul.nav-links
- opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
@@ -56,11 +58,6 @@
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public
- .nav-controls
- = render 'shared/projects/dropdown'
- = link_to new_project_path, class: 'btn btn-new' do
- New Project
-
.projects-list-holder
- if @projects.any?
%ul.projects-list.content-list
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 124f970524e..721bc77cc2f 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -26,7 +26,7 @@
.bs-callout
%p
- A 'Runner' is a process which runs a build.
+ A 'Runner' is a process which runs a job.
You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, even on your local machine.
@@ -37,16 +37,16 @@
%ul
%li
%span.label.label-success shared
- \- Runner runs builds from all unassigned projects
+ \- Runner runs jobs from all unassigned projects
%li
%span.label.label-info specific
- \- Runner runs builds from assigned projects
+ \- Runner runs jobs from assigned projects
%li
%span.label.label-warning locked
\- Runner cannot be assigned to other projects
%li
%span.label.label-danger paused
- \- Runner will not receive any new builds
+ \- Runner will not receive any new jobs
.append-bottom-20.clearfix
.pull-left
@@ -68,7 +68,7 @@
%th Runner token
%th Description
%th Projects
- %th Builds
+ %th Jobs
%th Tags
%th Last contact
%th
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 39e103e3062..dc4116e1ce0 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,13 +11,13 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This Runner will process builds from ALL UNASSIGNED projects
+ %h4 This Runner will process jobs from ALL UNASSIGNED projects
%p
If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This Runner will process builds only from ASSIGNED projects
+ %h4 This Runner will process jobs only from ASSIGNED projects
%p You can't make this a shared Runner.
%hr
@@ -70,11 +70,11 @@
= paginate @projects, theme: "gitlab"
.col-md-6
- %h4 Recent builds served by this Runner
+ %h4 Recent jobs served by this Runner
%table.table.ci-table.runner-builds
%thead
%tr
- %th Build
+ %th Job
%th Status
%th Project
%th Commit
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 4ce4eab8753..33f6d847782 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -14,6 +14,8 @@
%td
= spam_log.via_api? ? 'Y' : 'N'
%td
+ = spam_log.recaptcha_verified ? 'Y' : 'N'
+ %td
= spam_log.noteable_type
%td
= spam_log.title
diff --git a/app/views/admin/spam_logs/index.html.haml b/app/views/admin/spam_logs/index.html.haml
index 0fdd5bd9960..8aaa6379730 100644
--- a/app/views/admin/spam_logs/index.html.haml
+++ b/app/views/admin/spam_logs/index.html.haml
@@ -10,6 +10,7 @@
%th User
%th Source IP
%th API?
+ %th Recaptcha verified?
%th Type
%th Title
%th Description
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
new file mode 100644
index 00000000000..7855239dfe5
--- /dev/null
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -0,0 +1,37 @@
+%fieldset
+ %legend Access
+ .form-group
+ = f.label :projects_limit, class: 'control-label'
+ .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+
+ .form-group
+ = f.label :can_create_group, class: 'control-label'
+ .col-sm-10= f.check_box :can_create_group
+
+ .form-group
+ = f.label :access_level, class: 'control-label'
+ .col-sm-10
+ - editing_current_user = (current_user == @user)
+
+ = f.radio_button :access_level, :regular, disabled: editing_current_user
+ = label_tag :regular do
+ Regular
+ %p.light
+ Regular users have access to their groups and projects
+
+ = f.radio_button :access_level, :admin, disabled: editing_current_user
+ = label_tag :admin do
+ Admin
+ %p.light
+ Administrators have access to all groups, projects and users and can manage all features in this installation
+ - if editing_current_user
+ %p.light
+ You cannot remove your own admin rights.
+
+ .form-group
+ = f.label :external, class: 'control-label'
+ .col-sm-10
+ = f.check_box :external do
+ External
+ %p.light
+ External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 3145212728f..e911af3f6f9 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -40,28 +40,7 @@
= f.label :password_confirmation, class: 'control-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
- %fieldset
- %legend Access
- .form-group
- = f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
-
- .form-group
- = f.label :can_create_group, class: 'control-label'
- .col-sm-10= f.check_box :can_create_group
-
- .form-group
- = f.label :admin, class: 'control-label'
- - if current_user == @user
- .col-sm-10= f.check_box :admin, disabled: true
- .col-sm-10 You cannot remove your own admin rights.
- - else
- .col-sm-10= f.check_box :admin
-
- .form-group
- = f.label :external, class: 'control-label'
- .col-sm-10= f.check_box :external
- .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups.
+ = render partial: 'access_levels', locals: { f: f }
%fieldset
%legend Profile
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index b0bee1c6204..dfbc7772698 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -11,7 +11,7 @@
.form-group
.col-sm-12
.file-holder
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 8dea3479f82..8ed23ac4919 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -16,4 +16,4 @@
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
- = icon(status.action_icon, class: status.action_class)
+ = custom_icon(status.action_icon)
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
index dd2f649de9a..0530d21a7e2 100644
--- a/app/views/ci/status/_graph_badge.html.haml
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -2,7 +2,7 @@
- subject = local_assigns.fetch(:subject)
- status = subject.detailed_status(current_user)
-- klass = "ci-status-icon ci-status-icon-#{status.group}"
+- klass = "ci-status-icon ci-status-icon-#{status.group} js-ci-status-icon-#{status.group}"
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
@@ -16,5 +16,5 @@
- if status.has_action?
= link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
- %i.ci-action-icon-wrapper
- = icon(status.action_icon, class: status.action_class)
+ %i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" }
+ = custom_icon(status.action_icon)
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 3caaf827ff5..653052f7c54 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -15,6 +15,4 @@
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
-
-.prepend-top-default
- = render 'shared/issues'
+= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index fb016599fef..e64c78c4cb8 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -7,6 +7,4 @@
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
-
-.prepend-top-default
- = render 'shared/merge_requests'
+= render 'shared/merge_requests'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 9d7bcdb9d16..605bfd0cf8d 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -11,8 +11,11 @@
= link_to_author(todo)
- else
(removed)
- %span.todo-label
+
+ %span.action-name
= todo_action_name(todo)
+
+ %span.todo-label
- if todo.target
= todo_target_link(todo)
- else
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f4efcfb27b2..c4bf2c90cc2 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -67,21 +67,17 @@
= sort_title_oldest_created
-.prepend-top-default
+.js-todos-all
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- - @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
-
+ .panel.panel-default.panel-small.panel-without-border
%ul.content-list.todos-list
- = render group[1]
+ = render @todos
= paginate @todos, theme: "gitlab"
+
- elsif current_user.todos.any?
.todos-all-done
- = render "shared/empty_states/todos_all_done.svg"
+ = render "shared/empty_states/icons/todos_all_done.svg"
- if todos_filter_empty?
%h4.text-center
= Gitlab.config.gitlab.no_todos_messages.sample
@@ -98,7 +94,7 @@
- else
.todos-empty
.todos-empty-hero
- = render "shared/empty_states/todos_empty.svg"
+ = render "shared/empty_states/icons/todos_empty.svg"
.todos-empty-content
%h4
Todos let you see what you should do next.
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index e87a16a5157..f92f89e73ff 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -6,4 +6,4 @@
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+ = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn')
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 01ecf237925..5a44ec45b7b 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- - if current_application_settings.recaptcha_enabled
+ - if Gitlab::Recaptcha.enabled?
= recaptcha_tags
%div
= f.submit "Register", class: "btn-register btn"
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index 3a95a652810..94408b92374 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -2,7 +2,7 @@
- blob = discussion.blob
.diff-file.file-holder
- .file-title
+ .js-file-title.file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
.diff-content.code.js-syntax-highlight
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index f0b61e0f7de..e30ee1b0e05 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,6 +1,5 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
- ":discussion-id" => "'#{discussion.id}'",
+ %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
new file mode 100644
index 00000000000..41f54f6bf42
--- /dev/null
+++ b/app/views/groups/_home_panel.html.haml
@@ -0,0 +1,17 @@
+.group-home-panel.text-center
+ %div{ class: container_class }
+ .avatar-container.s70.group-avatar
+ = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
+ %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)
+
+ - if @group.description.present?
+ .group-home-desc
+ = 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
diff --git a/app/views/groups/_show_nav.html.haml b/app/views/groups/_show_nav.html.haml
new file mode 100644
index 00000000000..b2097e88741
--- /dev/null
+++ b/app/views/groups/_show_nav.html.haml
@@ -0,0 +1,7 @@
+%ul.nav-links
+ = nav_link(page: group_path(@group)) do
+ = link_to group_path(@group) do
+ Projects
+ = nav_link(page: subgroups_group_path(@group)) do
+ = link_to subgroups_group_path(@group) do
+ Subgroups
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index b185b81db7f..5b1a4630c56 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -3,7 +3,7 @@
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true)
.help-block.append-bottom-10
- Search for users by name, username, or email, or invite new ones using their email address.
+ Search for members by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
@@ -16,7 +16,7 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
- On this date, the user(s) will automatically lose access to this group and all of its projects.
+ On this date, the member(s) will automatically lose access to this group and all of its projects.
.col-md-2
= f.submit 'Add to group', class: "btn btn-create btn-block"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index f4c432a095a..2e4e4511bb6 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -7,7 +7,7 @@
- if can?(current_user, :admin_group_member, @group)
.project-members-new.append-bottom-default
%p.clearfix
- Add new user to
+ Add new member to
%strong= @group.name
= render "new_group_member"
@@ -15,7 +15,7 @@
.append-bottom-default.clearfix
%h5.member.existing-title
- Existing users
+ Existing members
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
@@ -24,7 +24,7 @@
= render 'shared/members/sort_dropdown'
.panel.panel-default
.panel-heading
- Users with access to
+ Members with access to
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 6ad03a60b3a..83edb719692 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -23,7 +23,6 @@
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
- .prepend-top-default
- = render 'shared/issues'
+ = render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index af73554086b..6ad76d23df5 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -15,5 +15,4 @@
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
-.prepend-top-default
- = render 'shared/merge_requests'
+= render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fb6f0da28f8..e66a8e0a3b3 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,8 @@
= render "header_title"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
= render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index d256d14609e..b040f404ac4 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -4,38 +4,12 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.group-home-panel.text-center
- %div{ class: container_class }
- .avatar-container.s70.group-avatar
- = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
- %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)
+= render 'groups/home_panel'
- - if @group.description.present?
- .group-home-desc
- = 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
.groups-header{ class: container_class }
.top-area
- %ul.nav-links
- %li.active
- = link_to "#projects", 'data-toggle' => 'tab' do
- All Projects
- - if @shared_projects.present?
- %li
- = link_to "#shared", 'data-toggle' => 'tab' do
- Shared Projects
- - if @nested_groups.present?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Subgroups
+ = render 'groups/show_nav'
.nav-controls
= form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
@@ -44,15 +18,4 @@
= link_to new_project_path(namespace_id: @group.id), class: 'btn btn-new pull-right' do
New Project
- .tab-content
- .tab-pane.active#projects
- = render "projects", projects: @projects
-
- - if @shared_projects.present?
- .tab-pane#shared
- = render "shared_projects", projects: @shared_projects
-
- - if @nested_groups.present?
- .tab-pane#groups
- %ul.content-list
- = render partial: 'shared/groups/group', collection: @nested_groups
+ = render "projects", projects: @projects
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
new file mode 100644
index 00000000000..8610ae7e0ef
--- /dev/null
+++ b/app/views/groups/subgroups.html.haml
@@ -0,0 +1,20 @@
+- @no_container = true
+
+= render 'groups/home_panel'
+
+.groups-header{ class: container_class }
+ .top-area
+ = render 'groups/show_nav'
+ .nav-controls
+ = form_tag request.path, method: :get do |f|
+ = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
+ - if can? current_user, :admin_group, @group
+ = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
+ New Subgroup
+
+ - if @nested_groups.present?
+ %ul.content-list
+ = render partial: 'shared/groups/group', collection: @nested_groups, locals: { full_name: false }
+ - else
+ .nothing-here-block
+ There are no subgroups to show.
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index b74cc822295..705e20112fa 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -79,6 +79,14 @@
%td.shortcut
.key esc
%td Go back
+ %tbody
+ %tr
+ %th
+ %th Project File
+ %tr
+ %td.shortcut
+ .key y
+ %td Go to file permalink
.col-lg-4
%table.shortcut-mappings
@@ -143,7 +151,7 @@
.key g
.key b
%td
- Go to builds
+ Go to jobs
%tr
%td.shortcut
.key g
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index dd1df46792b..87f9b503989 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -528,7 +528,7 @@
- blob = Snippet.new(content: "Wow\nSuch\nFile")
.example
.file-holder
- .file-title
+ .js-file-title.file-title
Awesome file
.file-actions
.btn-group
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 7f1b9ee7141..e18bd47798b 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -82,7 +82,7 @@
rather than Git. Please convert
= link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
and go through the
- = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
+ = link_to 'import flow', status_import_bitbucket_path
again.
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 3096f0ee19e..f2d355587bd 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,11 +28,13 @@
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
- = javascript_include_tag "application"
+ = javascript_include_tag(*webpack_asset_paths("application"))
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
+ = yield :project_javascripts
+
= csrf_meta_tags
- unless browser.safari?
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 935517d4913..248d439cd05 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -4,9 +4,6 @@
%body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data
- -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body.
- = yield :scripts_body_top
-
= render "layouts/header/default", title: header_title
= render 'layouts/page', sidebar: sidebar, nav: nav
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 9ecc0d11c95..59082ce5fd5 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,4 +1,4 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+%header.navbar.navbar-gitlab{ class: nav_header_class }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index ac04f57e217..19a947af4ca 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,5 +1,5 @@
+= render 'layouts/nav/admin_settings'
.scrolling-tabs-container{ class: nav_control_class }
- = render 'layouts/nav/admin_settings'
.fade-left
= icon('angle-left')
.fade-right
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index a8bbd67de80..7883823b21e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -96,8 +96,8 @@
-# Shortcut to builds page
- if project_nav_tab? :builds
%li.hidden
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- Builds
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ Jobs
-# Shortcut to commits page
- if project_nav_tab? :commits
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index c6df66d2c3c..665725f6862 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -18,19 +18,11 @@
Protected Branches
- if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners' do
- %span
- Runners
- = nav_link(controller: :variables) do
- = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
- %span
- Variables
- = nav_link(controller: :triggers) do
- = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
- %span
- Triggers
- = nav_link(controller: :pipelines_settings) do
- = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+ = nav_link(controller: :ci_cd) do
+ = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
+ = nav_link(controller: :pages) do
+ = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
+ %span
+ Pages
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 277eb71ea73..f5e7ea7710d 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -3,7 +3,7 @@
- header_title project_title(@project) unless header_title
- nav "project"
-- content_for :scripts_body_top do
+- content_for :project_javascripts do
- project = @target_project || @project
- if @project_wiki && @page
- preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index a744c4be9d6..060b50ffc69 100644
--- a/app/views/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,6 +1,6 @@
- content_for :header do
%h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (build failed)
+ GitLab (job failed)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
index 9d497983498..2a94688a6b0 100644
--- a/app/views/notify/build_fail_email.text.erb
+++ b/app/views/notify/build_fail_email.text.erb
@@ -1,4 +1,4 @@
-Build failed for <%= @project.name %>
+Job failed for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 8c2e6db1426..ca0eaa96a9d 100644
--- a/app/views/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -1,6 +1,6 @@
- content_for :header do
%h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (build successful)
+ GitLab (job successful)
%h3
Project:
@@ -21,4 +21,4 @@
Message: #{@build.pipeline.git_commit_message}
%p
- Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
+ Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
index c5ed4f84861..445cd46e64f 100644
--- a/app/views/notify/build_success_email.text.erb
+++ b/app/views/notify/build_success_email.text.erb
@@ -1,4 +1,4 @@
-Build successful for <%= @project.name %>
+Job successful for <%= @project.name %>
Status: <%= @build.status %>
Commit: <%= @build.pipeline.short_sha %>
diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb
index f495a2e5486..741c7f344c8 100644
--- a/app/views/notify/links/ci/builds/_build.text.erb
+++ b/app/views/notify/links/ci/builds/_build.text.erb
@@ -1 +1 @@
-Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
+Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> )
diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
index 8e89c52a1f3..af8924bad57 100644
--- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
+++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb
@@ -1 +1 @@
-Build #<%= build.id %>
+Job #<%= build.id %>
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
index 943ebdaeffe..1df04ea614e 100644
--- a/app/views/profiles/_head.html.haml
+++ b/app/views/profiles/_head.html.haml
@@ -1,3 +1,3 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/cropper.js')
- = page_specific_javascript_tag('profile/profile_bundle.js')
+ = page_specific_javascript_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 14b330d16ad..a4f4079d556 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -82,7 +82,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
%hr
- if current_user.can_change_username?
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 60a561c9f9c..2c006e1712d 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -85,11 +85,17 @@
:javascript
- var date = $('#personal_access_token_expires_at').val();
-
- var datepicker = $(".datepicker").datepicker({
- dateFormat: "yy-mm-dd",
- minDate: 0
+ var $dateField = $('#personal_access_token_expires_at');
+ var date = $dateField.val();
+
+ new Pikaday({
+ field: $dateField.get(0),
+ theme: 'gitlab-theme',
+ format: 'YYYY-MM-DD',
+ minDate: new Date(),
+ onSelect: function(dateText) {
+ $dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
+ }
});
$("#created-personal-access-token").click(function() {
diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml
index e2b73cee5a9..a41791f0eca 100644
--- a/app/views/projects/_customize_workflow.html.haml
+++ b/app/views/projects/_customize_workflow.html.haml
@@ -3,6 +3,6 @@
%h4
Customize your workflow!
%p
- Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
+ Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
= link_to "Get started", edit_project_path(@project), class: "btn btn-success"
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 1c3bccccb5c..a08436715d2 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -10,6 +10,7 @@
- if @project && event.project != @project
%span at
%strong= link_to_project event.project
+ = clipboard_button(clipboard_text: event.ref_name, class: 'btn-clipboard btn-transparent', title: 'Copy branch to clipboard')
#{time_ago_with_tooltip(event.created_at)}
.pull-right
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 1a1327fb53c..27d25a6b682 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -4,10 +4,10 @@
.checkbox.builds-feature
= form.label :only_allow_merge_if_build_succeeds do
= form.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
+ %strong Only allow merge requests to be merged if the pipeline succeeds
%br
%span.descr
- Builds need to be configured to enable this feature.
+ Pipelines need to be configured to enable this feature.
= link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')
.checkbox
= form.label :only_allow_merge_if_all_discussions_are_resolved do
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index d0ff14e45e6..edf55d59f28 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs'
.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 23f54553014..8a40281e28c 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,7 +7,7 @@
#blob-content-holder.tree-holder
.file-holder
- .file-title
+ .js-file-title.file-title
= blob_icon @blob.mode, @blob.name
%strong
= @path
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index ff893ea74e1..7b9cfbbd067 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,3 +1,6 @@
+.btn-group
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
.btn-group.tree-btn-group
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
@@ -12,7 +15,7 @@
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm'
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user
.btn-group{ role: "group" }
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index f75f438ee4f..19fa4c78501 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -24,7 +24,7 @@
#blob-content-holder.blob-content-holder
%article.file-holder
- .file-title
+ .js-file-title.file-title
= blob_icon blob.mode, blob.name
%strong
= blob.name
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 228ac61fc8c..e7adef5558a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,5 +1,5 @@
.file-holder.file.append-bottom-default
- .file-title.clearfix
+ .js-file-title.file-title.clearfix
.editor-ref
= icon('code-fork')
= ref
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 538f8591f13..3b1a2e54ec2 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -27,4 +27,4 @@
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
%tr.line_holder{ id: @form.to, class: line_class }
- = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true
+ = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a5dcd93f42e..8853801016b 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
- page_title "Edit", @blob.path, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index b6ed9518c48..e0ce8cc9601 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New File", @path.presence, @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+ = page_specific_javascript_bundle_tag('blob_edit')
%h3.page-title
New File
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 356bd50f7f3..f5ca9607823 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -3,8 +3,8 @@
- 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?
+ = page_specific_javascript_bundle_tag('boards')
+ = page_specific_javascript_bundle_tag('simulate_drag') 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"
@@ -24,5 +24,13 @@
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
+ %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'),
+ "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project),
+ "milestone-path" => milestones_filter_dropdown_path,
+ "label-path" => labels_filter_path,
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":project-id" => @project.try(:id) }
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index a2e5118a9f3..72bce4049de 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -29,6 +29,7 @@
":loading" => "list.loading",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
"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
index 34fdb1f6a74..f413a5e94c1 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -34,6 +34,7 @@
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index e4c2aff46ec..891c2c46251 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -4,25 +4,7 @@
"@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, alt: "Avatar" }
- %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 }}
+ %issue-card-inner{ ":list" => "list",
+ ":issue" => "issue",
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath" }
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
index df7fa9ddaf2..24d76da6f06 100644
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -22,3 +22,5 @@
= render "projects/boards/components/sidebar/due_date"
= render "projects/boards/components/sidebar/labels"
= render "projects/boards/components/sidebar/notifications"
+ %remove-btn{ ":issue" => "issue",
+ ":list" => "list" }
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 736b485bf06..27e81c2bec3 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,7 +1,7 @@
.content-block.build-header
.header-content
= render 'ci/status/badge', status: @build.detailed_status(current_user), link: false
- Build
+ Job
%strong.js-build-id ##{@build.id}
in pipeline
= link_to pipeline_path(@build.pipeline) do
@@ -17,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-secondary pull-right', method: :post
+ = link_to "Retry job", 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/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 37bf085130a..78720d88e4e 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,8 +1,8 @@
- builds = @build.pipeline.builds.to_a
-%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
+%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } }
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
- Build
+ Job
%strong ##{@build.id}
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
@@ -17,7 +17,7 @@
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
.title
- Build artifacts
+ Job artifacts
- if @build.artifacts_expired?
%p.build-detail-row
The artifacts were removed
@@ -42,9 +42,9 @@
.block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
.title
- Build details
+ Job details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+ = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -136,4 +136,4 @@
- else
= build.id
- if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' }
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index 028664f5bba..acfdb250aff 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -2,14 +2,14 @@
- if builds.blank?
%div
- .nothing-here-block No builds to show
+ .nothing-here-block No jobs to show
- else
.table-holder
%table.table.ci-table.builds-page
%thead
%tr
%th Status
- %th Build
+ %th Job
%th Pipeline
- if admin
%th Project
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index c623e39b21f..5ffc0e20d10 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Builds"
+- page_title "Jobs"
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -14,7 +14,7 @@
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index c613e473e4c..228dad528ab 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "#{@build.name} (##{@build.id})", "Builds"
+- page_title "#{@build.name} (##{@build.id})", "Jobs"
- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
@@ -12,14 +12,14 @@
.bs-callout.bs-callout-warning
%p
- if no_runners_for_project?(@build.project)
- This build is stuck, because the project doesn't have any runners online assigned to it.
+ This job is stuck, because the project doesn't have any runners online assigned to it.
- elsif @build.tags.any?
- This build is stuck, because you don't have any active runners online with any of these tags assigned to them:
+ This job is stuck, because you don't have any active runners online with any of these tags assigned to them:
- @build.tags.each do |tag|
%span.label.label-primary
= tag
- else
- This build is stuck, because you don't have any active runners that can run this build.
+ This job is stuck, because you don't have any active runners that can run this job.
%br
Go to
@@ -37,14 +37,14 @@
- 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)}.
+ This job 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)}.
+ This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}.
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.
+ The deployment of this job 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)}
+ This job is creating a deployment to #{environment_link_for_build(@build.project, @build)}
- if environment.try(:last_deployment)
and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')}
@@ -52,9 +52,9 @@
- if @build.erased?
.erased.alert.alert-warning
- if @build.erased_by_user?
- Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
+ Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)}
- else
- Build has been erased #{time_ago_with_tooltip(@build.erased_at)}
+ Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
- else
#js-build-scroll.scroll-controls
.scroll-step
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index c1e496455d1..5ea85f9fd4c 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -32,10 +32,10 @@
= link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- if build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
- if retried
- = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
+ = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
.label-container
- if build.tags.any?
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 818a70f38f1..f852f2e3fd7 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -15,7 +15,7 @@
- else
%span.api.monospace API
- if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
+ %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
@@ -40,25 +40,8 @@
- else
Cant find HEAD commit for this branch
- %td.stage-cell
- - pipeline.stages.each do |stage|
- - if stage.status
- - detailed_status = stage.detailed_status(current_user)
- - icon_status = "#{detailed_status.icon}_borderless"
- - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
-
- .stage-container.dropdown.js-mini-pipeline-graph
- %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
- = custom_icon(icon_status)
- = icon('caret-down')
-
- %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- .arrow-up
- .js-builds-dropdown-list.scrollable-menu
-
- .js-builds-dropdown-loading.builds-dropdown-loading.hidden
- %span.fa.fa-spinner.fa-spin
-
+ %td
+ = render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
@@ -78,7 +61,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' }
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 6dba42d5226..4d0b7a5ca85 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -63,9 +63,10 @@
- if @commit.status
.well-segment.pipeline-info
%div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = ci_icon_for_status(@commit.status)
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+ = ci_icon_for_status(@commit.status)
Pipeline
- = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace"
+ = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
for
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
%span.ci-status-label
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 08d3443b3d0..6abff6aaf95 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -13,7 +13,7 @@
Pipeline
= link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
with
- = pluralize pipeline.statuses.count(:id), "build"
+ = pluralize pipeline.statuses.count(:id), "job"
- if pipeline.ref
for
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
@@ -44,7 +44,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 1164627fa11..aae2cb8a04b 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -1,15 +1,25 @@
-%div
- - if pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .table-holder.pipelines
- %table.table.ci-table.js-pipeline-table
- %thead
- %th.pipeline-status Status
- %th.pipeline-info Pipeline
- %th.pipeline-commit Commit
- %th.pipeline-stages Stages
- %th.pipeline-date
- %th.pipeline-actions
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
+#commit-pipeline-table-view{ data: { endpoint: endpoint } }
+.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+} }
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index 00e7cdd1729..ac93eac41ac 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -1,6 +1,5 @@
-- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title 'Pipelines', "#{@commit.title} (#{@commit.short_id})", 'Commits'
-= render "commit_box"
-
-= render "ci_menu"
-= render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc)
+= render 'commit_box'
+= render 'ci_menu'
+= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 7afd3d80ef5..d5fc283aa8d 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -9,7 +9,7 @@
= render "ci_menu"
- else
.block-connector
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index d94f23f5a38..08cb8a04413 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -22,9 +22,7 @@
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d76d48187cd..08236216421 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -23,6 +23,4 @@
- if @merge_request.present?
= link_to "View Open Merge Request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'prepend-left-10 btn'
- elsif create_mr_button?
- = link_to create_mr_path, class: 'prepend-left-10 btn' do
- = icon("plus")
- Create Merge Request
+ = link_to "Create Merge Request", create_mr_path, class: 'prepend-left-10 btn'
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 9c8f58d4aea..0dfc9fe20ed 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else
.light-well
.center
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index 479ce44f378..5405ff16bea 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
- page_title "Cycle Analytics"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
+ = page_specific_javascript_bundle_tag('cycle_analytics')
= render "projects/pipelines/head"
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 58c20e225c6..4b49bed835f 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,3 +1,4 @@
+- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index c37a33bbcd5..0232a09b4a8 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,11 +1,13 @@
+- environment = local_assigns.fetch(:environment, nil)
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
- .file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
+ .js-file-title.file-title-flex-parent
+ .file-header-content
+ = 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
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
- if editable_diff?(diff_file)
@@ -13,6 +15,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file.new_path, project)
+ = view_file_button(diff_commit.id, diff_file.new_path, project)
+ = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index ddec775b789..1dbfe830d52 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -10,13 +10,13 @@
- if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
= old_path
&rarr;
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
= new_path
- else
- %strong
+ %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= diff_file.new_path
- if diff_file.deleted_file
deleted
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index f361204ecac..074f1f634ae 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,15 +1,17 @@
/ Side-by-side diff view
.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- - last_line = 0
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
- - last_line = right.new_pos if right
%tr.line_holder.parallel
- if left
- - if left.meta?
+ - case left.type
+ - when 'match'
= diff_match_line left.old_pos, nil, text: left.text, view: :parallel
+ - when 'nonewline'
+ %td.old_line.diff-line-num
+ %td.line_content.match= left.text
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
@@ -21,8 +23,12 @@
%td.line_content.parallel
- if right
- - if right.meta?
+ - case right.type
+ - when 'match'
= diff_match_line nil, right.new_pos, text: left.text, view: :parallel
+ - when 'nonewline'
+ %td.new_line.diff-line-num
+ %td.line_content.match= right.text
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
@@ -37,5 +43,7 @@
- discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
- if discussion_left || discussion_right
= render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- - if !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true, view: :parallel
+ - if !diff_file.new_file && diff_file.diff_lines.any?
+ - last_line = diff_file.diff_lines.last
+ %tr.line_holder.parallel
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index f1d2d4bf268..2eea1db169a 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -4,13 +4,13 @@
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- - last_line = 0
- discussions = @grouped_diff_discussions unless @diff_notes_disabled
= render partial: "projects/diffs/line",
collection: diff_file.highlighted_diff_lines,
as: :line,
locals: { diff_file: diff_file, discussions: discussions }
- - last_line = diff_file.highlighted_diff_lines.last.new_pos
- - if !diff_file.new_file && last_line > 0
- = diff_match_line last_line, last_line, bottom: true
+ - if !diff_file.new_file && diff_file.highlighted_diff_lines.any?
+ - last_line = diff_file.highlighted_diff_lines.last
+ %tr.line_holder
+ = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 84787155168..9c5c1a6d707 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -63,7 +63,7 @@
.row
.col-md-9.project-feature.nested
- = feature_fields.label :builds_access_level, "Builds", class: 'label-light'
+ = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Submit, test and deploy your changes before merge
.col-md-3
= project_feature_access_select(:builds_access_level)
@@ -133,6 +133,7 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
= f.submit 'Save changes', class: "btn btn-save"
+
.row.prepend-top-default
%hr
.row.prepend-top-default
@@ -180,11 +181,13 @@
%p
The following items will NOT be exported:
%ul
- %li Build traces and artifacts
+ %li Job traces and artifacts
%li LFS objects
%li Container registry images
- %hr
+ %li CI variables
+ %li Any encrypted tokens
- if can? current_user, :archive_project, @project
+ %hr
.row.prepend-top-default
.col-lg-3
%h4.warning-title.prepend-top-0
diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml
index 69848123c17..14a2d627203 100644
--- a/app/views/projects/environments/_stop.html.haml
+++ b/app/views/projects/environments/_stop.html.haml
@@ -1,4 +1,4 @@
-- if can?(current_user, :create_deployment, environment) && environment.stoppable?
+- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 8c728eb0f6a..1f27d41ddd9 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag("environments/environments_bundle.js")
+ = page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 6e0d9456900..7036325fff8 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,14 +5,14 @@
%div{ class: container_class }
.top-area.adjust
.col-md-9
- %h3.page-title= @environment.name.capitalize
+ %h3.page-title= @environment.name
.col-md-3
.nav-controls
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- - if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
+ - if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
@@ -32,8 +32,8 @@
%tr
%th ID
%th Commit
- %th Build
- %th
+ %th Job
+ %th Created
%th.hidden-xs
= render @deployments
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 431253c1299..ef0dd0eda3c 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -4,7 +4,7 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
- = page_specific_javascript_tag("terminal/terminal_bundle.js")
+ = page_specific_javascript_bundle_tag("terminal")
%div{ class: container_class }
.top-area
@@ -16,6 +16,8 @@
.col-sm-6
.nav-controls
+ = link_to @environment.external_url, class: 'btn btn-default' do
+ = icon('external-link')
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.terminal-container{ class: container_class }
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 1a62a6a809c..67018aaa2ac 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -5,8 +5,8 @@
%ul{ class: (container_class) }
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_chart')
+ = page_specific_javascript_bundle_tag('graphs')
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 431657c4dcb..b6f453b9736 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -1,4 +1,4 @@
-%h4 Build charts
+%h4 Pipelines charts
%p
&nbsp;
%span.cgreen
@@ -11,19 +11,19 @@
.prepend-top-default
%p.light
- Builds for last week
+ Jobs for last week
(#{date_from_to(Date.today - 7.days, Date.today)})
%canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
- Builds for last month
+ Jobs for last month
(#{date_from_to(Date.today - 30.days, Date.today)})
%canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
- Builds for last year
+ Jobs for last year
%canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope|
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index c2f4457b60b..5d4e593e4ef 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index bd46af339cf..0e3902c066a 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,60 +1,46 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- - if @bulk_edit
- .issue-check
- = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-box
+ - if @bulk_edit
+ .issue-check
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ .issue-info-container
+ .issue-title.title
+ %span.issue-title-text
+ = confidential_icon(issue)
+ = link_to issue.title, issue_path(issue)
+ %ul.controls
+ - if issue.closed?
+ %li
+ CLOSED
- .issue-title.title
- %span.issue-title-text
- = confidential_icon(issue)
- = link_to issue.title, issue_path(issue)
- %ul.controls
- - if issue.closed?
- %li
- CLOSED
+ - if issue.assignee
+ %li
+ = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
- - if issue.assignee
- %li
- = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+ = render 'shared/issuable_meta_data', issuable: issue
- - upvotes, downvotes = issue.upvotes, issue.downvotes
- - if upvotes > 0
- %li
- = icon('thumbs-up')
- = upvotes
+ .issue-info
+ #{issuable_reference(issue)} &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
+ - if issue.milestone
+ &nbsp;
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
+ = icon('clock-o')
+ = issue.milestone.title
+ - if issue.due_date
+ %span{ class: "#{'cred' if issue.overdue?}" }
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
+ - if issue.labels.any?
+ &nbsp;
+ - issue.labels.each do |label|
+ = link_to_label(label, subject: issue.project, css_class: 'label-link')
+ - if issue.tasks?
+ &nbsp;
+ %span.task-status
+ = issue.task_status
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = issue.notes.user.count
- %li
- = link_to issue_path(issue, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .issue-info
- #{issue.to_reference} &middot;
- opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
- by #{link_to_member(@project, issue.author, avatar: false)}
- - if issue.milestone
- &nbsp;
- = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
- = icon('clock-o')
- = issue.milestone.title
- - if issue.due_date
- %span{ class: "#{'cred' if issue.overdue?}" }
- &nbsp;
- = icon('calendar')
- = issue.due_date.to_s(:medium)
- - if issue.labels.any?
- &nbsp;
- - issue.labels.each do |label|
- = link_to_label(label, subject: issue.project)
- - if issue.tasks?
- &nbsp;
- %span.task-status
- = issue.task_status
-
- .pull-right.issue-updated-at
- %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ .pull-right.issue-updated-at
+ %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 5fbed8b9ab8..8ea1a3a45e1 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -7,7 +7,7 @@
= render "projects/issues/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js')
+ = page_specific_javascript_bundle_tag('filtered_search')
= content_for :meta_tags do
- if current_user
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9fa00811af0..d3eb3b7055b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -3,7 +3,7 @@
- page_description @issue.description
- page_card_attributes @issue.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/vue_resource.js')
+ = page_specific_javascript_bundle_tag('lib_vue')
.clearfix.detail-page-header
.issuable-header
@@ -35,9 +35,9 @@
= link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- if can?(current_user, :update_issue, @issue)
%li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
%li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin?
@@ -48,8 +48,8 @@
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if @issue.submittable_as_spam? && current_user.admin?
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
@@ -75,7 +75,7 @@
// This element is filled in using JavaScript.
.content-block.content-block-small
- = render 'new_branch'
+ = render 'new_branch' unless @issue.confidential?
= render 'award_emoji/awards_block', awardable: @issue, inline: true
%section.issuable-discussion
diff --git a/app/views/projects/issues/verify.html.haml b/app/views/projects/issues/verify.html.haml
new file mode 100644
index 00000000000..1934b18c086
--- /dev/null
+++ b/app/views/projects/issues/verify.html.haml
@@ -0,0 +1,20 @@
+- page_title "Anti-spam verification"
+
+%h3.page-title
+ Anti-spam verification
+%hr
+
+%p
+ We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
+
+= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
+ .recaptcha
+ - params[:issue].each do |field, value|
+ = hidden_field(:issue, field, value: value)
+ = hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
+ = hidden_field_tag(:spam_log_id, @issue.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags
+
+ .row-content-block.footer-block
+ = f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
deleted file mode 100644
index 8d09e2bda11..00000000000
--- a/app/views/projects/labels/destroy.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- if @labels.empty?
- $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 05a8475dcd6..8d4a91cb64c 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,37 +3,38 @@
- hide_class = ''
= render "projects/issues/head"
-%div{ class: container_class }
- .top-area.adjust
- .nav-text
- Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
- .nav-controls
- - if can?(current_user, :admin_label, @project)
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
- New label
+- if @labels.exists? || @prioritized_labels.exists?
+ %div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ Labels can be applied to issues and merge requests. Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- .labels
- - if can?(current_user, :admin_label, @project)
- -# Only show it in the first page
- - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- .prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
- %p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- - if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+ .nav-controls
+ - if can?(current_user, :admin_label, @project)
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new" do
+ New label
- .other-labels
+ .labels
- if can?(current_user, :admin_label, @project)
- %h5{ class: ('hide' if hide) } Other Labels
- %ul.content-list.manage-labels-list.js-other-labels
- - if @labels.present?
- = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
- = paginate @labels, theme: 'gitlab'
- - if @labels.blank?
- .nothing-here-block
+ -# Only show it in the first page
+ - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
+ .prioritized-labels{ class: ('hide' if hide) }
+ %h5 Prioritized Labels
+ %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
+ #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
+ = render 'shared/empty_states/priority_labels'
+ - if @prioritized_labels.present?
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+
+ - if @labels.present?
+ .other-labels
- if can?(current_user, :admin_label, @project)
- Create a label or #{link_to 'generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post}.
- - else
- No labels created
+ %h5{ class: ('hide' if hide) } Other Labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
+ = paginate @labels, theme: 'gitlab'
+- else
+ = render 'shared/empty_states/labels'
diff --git a/app/views/projects/mattermosts/_no_teams.html.haml b/app/views/projects/mattermosts/_no_teams.html.haml
index 605c7f61dee..aac74a25b75 100644
--- a/app/views/projects/mattermosts/_no_teams.html.haml
+++ b/app/views/projects/mattermosts/_no_teams.html.haml
@@ -1,3 +1,7 @@
+- if @teams_error_message
+ = content_for :flash_message do
+ .alert.alert-danger= @teams_error_message
+
%p
You aren’t a member of any team on the Mattermost instance at
%strong= Gitlab.config.mattermost.host
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index e3b0aa7e644..11b7aaec704 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -3,73 +3,59 @@
.issue-check
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
- .merge-request-title.title
- %span.merge-request-title-text
- = link_to merge_request.title, merge_request_path(merge_request)
- %ul.controls
- - if merge_request.merged?
- %li
- MERGED
- - elsif merge_request.closed?
- %li
- = icon('ban')
- CLOSED
-
- - if merge_request.head_pipeline
- %li
- = render_pipeline_status(merge_request.head_pipeline)
-
- - if merge_request.open? && merge_request.broken?
- %li
- = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
- = icon('exclamation-triangle')
-
- - if merge_request.assignee
- %li
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
-
- - upvotes, downvotes = merge_request.upvotes, merge_request.downvotes
- - if upvotes > 0
- %li
- = icon('thumbs-up')
- = upvotes
-
- - if downvotes > 0
- %li
- = icon('thumbs-down')
- = downvotes
-
- - note_count = merge_request.related_notes.user.count
- %li
- = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
- = icon('comments')
- = note_count
-
- .merge-request-info
- #{merge_request.to_reference} &middot;
- opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
- by #{link_to_member(@project, merge_request.author, avatar: false)}
- - if merge_request.target_project.default_branch != merge_request.target_branch
- &nbsp;
- = 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
- = merge_request.task_status
-
- .pull-right.hidden-xs
- %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ .issue-info-container
+ .merge-request-title.title
+ %span.merge-request-title-text
+ = link_to merge_request.title, merge_request_path(merge_request)
+ %ul.controls
+ - if merge_request.merged?
+ %li
+ MERGED
+ - elsif merge_request.closed?
+ %li
+ = icon('ban')
+ CLOSED
+
+ - if merge_request.head_pipeline
+ %li
+ = render_pipeline_status(merge_request.head_pipeline)
+
+ - if merge_request.open? && merge_request.broken?
+ %li
+ = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = icon('exclamation-triangle')
+
+ - if merge_request.assignee
+ %li
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+
+ = render 'shared/issuable_meta_data', issuable: merge_request
+
+ .merge-request-info
+ #{issuable_reference(merge_request)} &middot;
+ opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, merge_request.author, avatar: false)}
+ - if merge_request.target_project.default_branch != merge_request.target_branch
+ &nbsp;
+ = 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, css_class: 'label-link')
+
+ - if merge_request.tasks?
+ &nbsp;
+ %span.task-status
+ = merge_request.task_status
+
+ .pull-right.hidden-xs
+ %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
index 74367ab9b7b..627fc4e9671 100644
--- a/app/views/projects/merge_requests/_new_diffs.html.haml
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -1 +1 @@
-= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index d3c013b3f21..bd72310c16b 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
- = render "projects/merge_requests/show/pipelines"
+ = render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= spinner
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 9585a9a3ad4..dd615d3036c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -3,10 +3,9 @@
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/vue_resource.js')
- = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
+ = page_specific_javascript_bundle_tag('diff_notes')
-.merge-request{ 'data-url' => merge_request_path(@merge_request) }
+.merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -94,7 +93,8 @@
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- -# This tab is always loaded via AJAX
+ - if @pipelines.any?
+ = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
#diffs.diffs.tab-pane
-# This tab is always loaded via AJAX
@@ -108,10 +108,10 @@
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
- var merge_request;
-
- merge_request = new MergeRequest({
- action: "#{controller.action_name}"
+ $(function () {
+ new MergeRequest({
+ action: "#{controller.action_name}"
+ });
});
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index ebef2157d34..1ecd9924d88 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,7 +1,7 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/vue_resource.js')
- = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_vue')
+ = page_specific_javascript_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/show/mr_title"
@@ -23,7 +23,7 @@
.files-wrapper{ "v-if" => "!isLoading && !hasError" }
.files
.diff-file.file-holder.conflict{ "v-for" => "file in conflictsData.files" }
- .file-title
+ .js-file-title.file-title
%i.fa.fa-fw{ ":class" => "file.iconClass" }
%strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions'
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
index 2595ce74ac0..0839880713f 100644
--- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -8,5 +8,5 @@
'@click' => "onClickResolveModeButton(file, 'edit')",
type: 'button' }
Edit inline
- %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" }
+ %a.btn.view-file{ ":href" => "file.blobPath" }
View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 5f048d04b27..7f0913ea516 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions'
- = render "projects/diffs/diffs", diffs: @diffs
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
index afe3f3430c6..de4aa255bbd 100644
--- a/app/views/projects/merge_requests/show/_pipelines.html.haml
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -1 +1,3 @@
-= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
+- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
+
+= render 'projects/commit/pipelines_list', endpoint: endpoint_path
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 5faa6c43f9f..e3062f47788 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,15 +1,21 @@
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
- = ci_icon_for_status(status)
+ .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
+ %div{ class: "ci-status-icon-#{status}" }
+ = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
+ = ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- for
- = succeed "." do
- = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
+ - if @pipeline.stages.any?
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
+ %span
+ for
+ = succeed "." do
+ = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
@@ -20,7 +26,7 @@
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
= ci_icon_for_status(status)
%span
- CI build
+ CI job
= ci_label_for_status(status)
for
- commit = @merge_request.diff_head_commit
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 38328501ffd..0b0fb7854c2 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -16,14 +16,18 @@
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
- normal: "Build {{status}} for \"{{title}}\"",
- preparing: "{{status}} build for \"{{title}}\""
+ normal: "Pipeline {{status}} for \"{{title}}\"",
+ preparing: "{{status}} pipeline for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
- preparing: "{{status}} build",
- normal: "Build {{status}}"
+ preparing: "{{status}} pipeline",
+ normal: "Pipeline {{status}}"
},
+ ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
+ ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
+ commits_path: "#{project_commits_path(@project)}",
+ pipeline_path: "#{project_pipelines_path(@project)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index 7809e9c8c72..b730ced4214 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,5 +1,5 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+ = page_specific_javascript_bundle_tag('merge_request_widget')
- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
@@ -35,10 +35,10 @@
The source branch will be removed.
- elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
- = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
+ = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
- .accept-control.right
+ .accept-control
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
index 14f51af5360..3979d5fa8ed 100644
--- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml
@@ -1,6 +1,6 @@
%h4
= icon('exclamation-triangle')
- The build for this merge request failed
+ The pipeline for this merge request failed
%p
- Please retry the build or push a new commit to fix the failure.
+ Please retry the job or push a new commit to fix the failure.
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index 50086767446..909dc52fc06 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -1,5 +1,5 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+ = page_specific_javascript_bundle_tag('merge_request_widget')
%strong
= icon("spinner spin")
diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
index f70cd09c5f4..cf7abf3756c 100644
--- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -1,5 +1,5 @@
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
+ = page_specific_javascript_bundle_tag('merge_request_widget')
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
@@ -19,7 +19,7 @@
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
- = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
+ = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove Source Branch When Merged
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index c3a6096aa54..06a31698ee6 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,6 +3,9 @@
- page_description @milestone.description
= render "projects/issues/head"
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
+
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index d8951e69242..b88eef65cef 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,7 @@
- page_title "Network", @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/raphael.js')
- = page_specific_javascript_tag('network/network_bundle.js')
+ = page_specific_javascript_bundle_tag('network')
= render "projects/commits/head"
= render "head"
%div{ class: container_class }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 064e92b15eb..41473fae4de 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -50,7 +50,7 @@
= icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
= icon('bitbucket', text: 'Bitbucket')
- unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
@@ -94,9 +94,8 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
- = link_to "(?)", help_page_path("public_access/public_access")
- = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
-
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
= 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 09339e520dd..e58de9f0e18 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -9,9 +9,12 @@
= image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- = link_to_member(note.project, note.author, avatar: false)
- .note-headline-light
+ %a.visible-xs{ href: user_path(note.author) }
= note.author.to_reference
+ = link_to_member(note.project, note.author, avatar: false, extra_class: 'hidden-xs')
+ .note-headline-light
+ %span.hidden-xs
+ = note.author.to_reference
- unless note.system
commented
- if note.system
@@ -23,12 +26,11 @@
.note-actions
- access = note_max_access_for_user(note)
- if access
- %span.note-role.hidden-xs= access
+ %span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "project-path" => "#{project_path(note.project)}",
- "discussion-id" => "#{note.discussion_id}",
+ %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
@@ -59,7 +61,7 @@
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index fbd2bff5bbb..08c73d94a09 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -13,7 +13,7 @@
= image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40'
.timeline-content.timeline-content-form
= render "projects/notes/form", view: diff_view
- - else
+ - elsif !current_user
.disabled-comment.text-center
.disabled-comment-text.inline
Please
diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml
new file mode 100644
index 00000000000..82e20eeebb3
--- /dev/null
+++ b/app/views/projects/pages/_access.html.haml
@@ -0,0 +1,13 @@
+- if @project.pages_deployed?
+ .panel.panel-default
+ .panel-heading
+ Access pages
+ .panel-body
+ %p
+ %strong
+ Congratulations! Your pages are served under:
+
+ %p= link_to @project.pages_url, @project.pages_url
+
+ - @project.pages_domains.each do |domain|
+ %p= link_to domain.url, domain.url
diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml
new file mode 100644
index 00000000000..42d9ef5ccba
--- /dev/null
+++ b/app/views/projects/pages/_destroy.haml
@@ -0,0 +1,12 @@
+- if @project.pages_deployed?
+ - if can?(current_user, :remove_pages, @project)
+ .panel.panel-default.panel.panel-danger
+ .panel-heading Remove pages
+ .errors-holder
+ .panel-body
+ %p
+ Removing the pages will prevent from exposing them to outside world.
+ .form-actions
+ = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove"
+ - else
+ .nothing-here-block Only the project owner can remove pages
diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml
new file mode 100644
index 00000000000..ad51fbc6cab
--- /dev/null
+++ b/app/views/projects/pages/_disabled.html.haml
@@ -0,0 +1,4 @@
+.panel.panel-default
+ .nothing-here-block
+ GitLab Pages are disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
new file mode 100644
index 00000000000..4f2dd1a1398
--- /dev/null
+++ b/app/views/projects/pages/_list.html.haml
@@ -0,0 +1,17 @@
+- if can?(current_user, :update_pages, @project) && @domains.any?
+ .panel.panel-default
+ .panel-heading
+ Domains (#{@domains.count})
+ %ul.well-list
+ - @domains.each do |domain|
+ %li
+ .pull-right
+ = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+ .clearfix
+ %span= link_to domain.domain, domain.url
+ %p
+ - if domain.subject
+ %span.label.label-gray Certificate: #{domain.subject}
+ - if domain.expired?
+ %span.label.label-danger Expired
diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml
new file mode 100644
index 00000000000..7cea5f3e70b
--- /dev/null
+++ b/app/views/projects/pages/_no_domains.html.haml
@@ -0,0 +1,7 @@
+- if can?(current_user, :update_pages, @project)
+ .panel.panel-default
+ .panel-heading
+ Domains
+ .nothing-here-block
+ Support for domains and certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml
new file mode 100644
index 00000000000..9db46f0b1fc
--- /dev/null
+++ b/app/views/projects/pages/_use.html.haml
@@ -0,0 +1,8 @@
+- unless @project.pages_deployed?
+ .panel.panel-info
+ .panel-heading
+ Configure pages
+ .panel-body
+ %p
+ Learn how to upload your static site and have it served by
+ GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}.
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
new file mode 100644
index 00000000000..b6595269b06
--- /dev/null
+++ b/app/views/projects/pages/show.html.haml
@@ -0,0 +1,26 @@
+- page_title 'Pages'
+%h3.page_title
+ Pages
+
+ - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https)
+ = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do
+ %i.fa.fa-plus
+ New Domain
+
+%p.light
+ With GitLab Pages you can host your static websites on GitLab.
+ Combined with the power of GitLab CI and the help of GitLab Runner
+ you can deploy static pages for your individual projects, your user or your group.
+
+%hr.clearfix
+
+- if Gitlab.config.pages.enabled
+ = render 'access'
+ = render 'use'
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
+ = render 'list'
+ - else
+ = render 'no_domains'
+ = render 'destroy'
+- else
+ = render 'disabled'
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
new file mode 100644
index 00000000000..ca1b41b140a
--- /dev/null
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -0,0 +1,34 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ - if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+
+ .form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
+
+ - if Gitlab.config.pages.external_https
+ .form-group
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
+ .col-sm-10
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
+
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+ - else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
+
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
new file mode 100644
index 00000000000..e1477c71d06
--- /dev/null
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -0,0 +1,6 @@
+- page_title 'New Pages Domain'
+%h3.page_title
+ New Pages Domain
+%hr.clearfix
+%div
+ = render 'form'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
new file mode 100644
index 00000000000..52dddb052a7
--- /dev/null
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -0,0 +1,30 @@
+- page_title "#{@domain.domain}", 'Pages Domains'
+
+%h3.page-title
+ Pages Domain
+
+.table-holder
+ %table.table
+ %tr
+ %td
+ Domain
+ %td
+ = link_to @domain.domain, @domain.url
+ %tr
+ %td
+ DNS
+ %td
+ %p
+ To access the domain create a new DNS record:
+ %pre
+ #{@domain.domain} CNAME #{@domain.project.namespace.path}.#{Settings.pages.host}.
+ %tr
+ %td
+ Certificate
+ %td
+ - if @domain.certificate_text
+ %pre
+ = @domain.certificate_text
+ - else
+ .light
+ missing
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index b10dd47709f..721a9b6beb5 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -11,9 +11,9 @@
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
- Builds
+ Jobs
- if project_nav_tab? :environments
= nav_link(controller: %w(environments)) do
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index ca76f13ef5e..a6cd2d83bd5 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -23,9 +23,9 @@
.info-well
- if @commit.status
.well-segment.pipeline-info
- %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = ci_icon_for_status(@commit.status)
- = pluralize @pipeline.statuses.count(:id), "build"
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.statuses.count(:id), "job"
- if @pipeline.ref
from
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 88af41aa835..53067cdcba4 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -5,7 +5,7 @@
Pipeline
%li.js-builds-tab-link
= link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
- Builds
+ Jobs
%span.badge.js-builds-counter= pipeline.statuses.count
@@ -33,7 +33,7 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Job ID
%th Name
%th
- if pipeline.project.build_coverage_enabled?
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index df36279ed75..81e393d7626 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -36,32 +36,28 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
.content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } }
- - if @pipelines.blank?
- %div
- .nothing-here-block No pipelines to show
- - else
- .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
- "icon_status_canceled" => custom_icon("icon_status_canceled"),
- "icon_status_running" => custom_icon("icon_status_running"),
- "icon_status_skipped" => custom_icon("icon_status_skipped"),
- "icon_status_created" => custom_icon("icon_status_created"),
- "icon_status_pending" => custom_icon("icon_status_pending"),
- "icon_status_success" => custom_icon("icon_status_success"),
- "icon_status_failed" => custom_icon("icon_status_failed"),
- "icon_status_warning" => custom_icon("icon_status_warning"),
- "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
- "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
- "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
- "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
- "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
- "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
- "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
- "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
- "icon_play" => custom_icon("icon_play"),
- "icon_timer" => custom_icon("icon_timer"),
- "icon_status_manual" => custom_icon("icon_status_manual"),
- } }
+ .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"),
+ "icon_status_canceled" => custom_icon("icon_status_canceled"),
+ "icon_status_running" => custom_icon("icon_status_running"),
+ "icon_status_skipped" => custom_icon("icon_status_skipped"),
+ "icon_status_created" => custom_icon("icon_status_created"),
+ "icon_status_pending" => custom_icon("icon_status_pending"),
+ "icon_status_success" => custom_icon("icon_status_success"),
+ "icon_status_failed" => custom_icon("icon_status_failed"),
+ "icon_status_warning" => custom_icon("icon_status_warning"),
+ "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"),
+ "stage_icon_status_running" => custom_icon("icon_status_running_borderless"),
+ "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"),
+ "stage_icon_status_created" => custom_icon("icon_status_created_borderless"),
+ "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"),
+ "stage_icon_status_success" => custom_icon("icon_status_success_borderless"),
+ "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"),
+ "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"),
+ "icon_play" => custom_icon("icon_play"),
+ "icon_timer" => custom_icon("icon_timer"),
+ "icon_status_manual" => custom_icon("icon_status_manual"),
+ } }
.vue-pipelines-index
-= page_specific_javascript_tag('vue_pipelines_index/index.js')
+= page_specific_javascript_bundle_tag('vue_pipelines')
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 1f698558bce..8024fb8979d 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -1,9 +1,7 @@
-- page_title "CI/CD Pipelines"
-
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- = page_title
+ CI/CD Pipelines
.col-lg-9
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
@@ -66,7 +64,7 @@
%span.input-group-addon /
%p.help-block
A regular expression that will be used to find the test coverage
- output in the build trace. Leave blank to disable
+ output in the job trace. Leave blank to disable
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
.bs-callout.bs-callout-info
%p Below are examples of regex for existing tools:
@@ -95,4 +93,4 @@
%hr
.row.prepend-top-default
- = render partial: 'badge', collection: @badges
+ = render partial: 'projects/pipelines_settings/badge', collection: @badges
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 42e9bdbd30e..b3b419bd92d 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Protected branches"
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js')
+ = page_specific_javascript_bundle_tag('protected_branches')
.row.prepend-top-default.append-bottom-default
.col-lg-3
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 33a9a96183c..98e72f6c547 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
.col-sm-10
.checkbox
= f.check_box :active
- %span.light Paused Runners don't accept new builds
+ %span.light Paused Runners don't accept new jobs
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/_index.html.haml
index 92957470070..f9808f7c990 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/_index.html.haml
@@ -1,8 +1,6 @@
-- page_title "Runners"
-
.light.prepend-top-default
%p
- A 'Runner' is a process which runs a build.
+ A 'Runner' is a process which runs a job.
You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, and even on your local machine.
@@ -12,16 +10,16 @@
%ul
%li
%span.label.label-success active
- \- Runner is active and can process any new builds
+ \- Runner is active and can process any new jobs
%li
%span.label.label-danger paused
- \- Runner is paused and will not receive any new builds
+ \- Runner is paused and will not receive any new jobs
%hr
-%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
+%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners
.row
.col-sm-6
- = render 'specific_runners'
+ = render 'projects/runners/specific_runners'
.col-sm-6
- = render 'shared_runners'
+ = render 'projects/runners/shared_runners'
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 5afa193357e..0671dd66e78 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -22,7 +22,7 @@
- else
%h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
- = render partial: 'runner', collection: @shared_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
.light
and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index dcff675eafc..6b8e6bd4fee 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -20,10 +20,10 @@
- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @project_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @project_runners, as: :runner
- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner
= paginate @assignable_runners, theme: "gitlab"
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 8ca4c51a064..3a323d94cc2 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,16 +1,19 @@
-- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
-To setup this service:
-%ul.list-unstyled
+%p To setup this service:
+%ul.list-unstyled.indent-list
%li
1.
- = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Enable custom slash commands
+ = icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
- in Mattermost with these options:
-
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Mattermost team with these options:
%hr
.help-form
@@ -83,9 +86,14 @@ To setup this service:
%hr
-%ul.list-unstyled
+%ul.list-unstyled.indent-list
%li
- 3. After adding the slash command, paste the
-
- %strong token
+ 3. Paste the
+ %strong Token
into the field below
+ %li
+ 4. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Mattermost!
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index c1e576b42fc..a04fd5035a6 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,13 +1,16 @@
- enabled = Gitlab.config.mattermost.enabled
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Mattermost.
- %br
- See list of available commands in Mattermost after setting up this service,
- by entering
- %code /&lt;command_trigger_word&gt; help
-
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 04b9100acc6..0d973a20d4c 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,21 +1,25 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
-- run_actions_text = "Perform common operations on this project: #{pretty_name}"
+- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
- This service allows GitLab users to perform common operations on this
- project by entering slash commands in Slack.
- %br
- See list of available commands in Slack after setting up this service,
- by entering
- %code /&lt;command&gt; help
- %br
- %br
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
- unless @service.template?
- To setup this service:
- %ul.list-unstyled
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
%li
1.
- = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
in your Slack team with these options:
%hr
@@ -82,7 +86,7 @@
%hr
- %ul.list-unstyled
+ %ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
new file mode 100644
index 00000000000..52f5f7b81e2
--- /dev/null
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -0,0 +1,6 @@
+- page_title "CI/CD Pipelines"
+
+= render 'projects/runners/index'
+= render 'projects/variables/index'
+= render 'projects/triggers/index'
+= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 068a6610350..dde2e2b644d 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
@@ -8,6 +10,8 @@
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
@@ -27,3 +31,6 @@
%li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 216f70f5605..fb39028529d 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
Edit Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
+= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet)
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 772a594269c..cfed3a79bc5 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -3,4 +3,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet)
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 485b23815bc..6b3d7d4008b 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,7 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index a1f4e3e8ed6..bdcc160a067 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
%article.file-holder.readme-holder
- .file-title
+ .js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, @path, readme.name)) do
%strong
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/_index.html.haml
index 6e5dd1b196d..33883facf9b 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,9 +1,7 @@
-- page_title "Triggers"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- = page_title
+ Triggers
%p.prepend-top-20
Triggers can force a specific branch or tag to get rebuilt with an API call.
%p.append-bottom-0
@@ -25,12 +23,12 @@
%th
%strong Last used
%th
- = render partial: 'trigger', collection: @triggers, as: :trigger
+ = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
No triggers have been created yet. Add one using the button below.
- = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
+ = form_for @trigger, url: url_for(controller: '/projects/triggers', action: 'create') do |f|
= f.submit "Add trigger", class: 'btn btn-success'
.panel-footer
@@ -67,7 +65,7 @@
In the
%code .gitlab-ci.yml
of another project, include the following snippet.
- The project will be rebuilt at the end of the build.
+ The project will be rebuilt at the end of the pipeline.
%pre
:plain
@@ -86,12 +84,12 @@
:plain
#{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN
%h5.prepend-top-default
- Pass build variables
+ Pass job variables
%p.light
Add
%code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+ to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines.
With cURL:
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
index 0249e0c1bf1..06477aba103 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/projects/variables/_content.html.haml
@@ -5,4 +5,4 @@
%p
So you can use them for passwords, secret keys or whatever you want.
%p
- The value of the variable can be visible in build log if explicitly asked to do so.
+ The value of the variable can be visible in job log if explicitly asked to do so.
diff --git a/app/views/projects/variables/index.html.haml b/app/views/projects/variables/_index.html.haml
index cf7ae0b489f..1b852a9c5b3 100644
--- a/app/views/projects/variables/index.html.haml
+++ b/app/views/projects/variables/_index.html.haml
@@ -1,12 +1,10 @@
-- page_title "Variables"
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
- = render "content"
+ = render "projects/variables/content"
.col-lg-9
%h5.prepend-top-0
Add a variable
- = render "form", btn_text: "Add new variable"
+ = render "projects/variables/form", btn_text: "Add new variable"
%hr
%h5.prepend-top-0
Your variables (#{@project.variables.size})
@@ -14,5 +12,5 @@
%p.settings-message.text-center.append-bottom-0
No variables found, add one with the form above.
- else
- = render "table"
+ = render "projects/variables/table"
%button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index e3fddfba689..8c582f747b3 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,4 +1,4 @@
-%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar
+%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
.block.wiki-sidebar-header.append-bottom-default
%a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
= icon('angle-double-right')
diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index 9e8adc82583..7f1f807e2e7 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,7 +1,7 @@
- file_name, blob = blob
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
- ref = @search_results.repository_ref
- blob_link = namespace_project_blob_path(@project.namespace, @project, tree_join(ref, file_name))
= link_to blob_link do
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 23ca6479414..f7808ea6aff 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -14,7 +14,7 @@
- snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
.file-holder
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file
%strong= snippet.file_name
- if markup?(snippet.file_name)
diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml
index 648d0bd76cb..d87f9df2677 100644
--- a/app/views/search/results/_wiki_blob.html.haml
+++ b/app/views/search/results/_wiki_blob.html.haml
@@ -1,7 +1,7 @@
- wiki_blob = parse_search_result(wiki_blob)
.blob-result
.file-holder
- .file-title
+ .js-file-title.file-title
= link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do
%i.fa.fa-file
%strong
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index c196bc06b17..4b98ff88241 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -17,9 +17,9 @@
Try to keep the first line under 52 characters
and the others under 72.
- if descriptions.present?
- %p.hint.js-with-description-hint
+ .hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do
Include description in commit message
- %p.hint.js-without-description-hint.hide
+ .hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do
Don't include description in commit message
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 0bc851b4256..efb207b9916 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,3 +1,4 @@
+- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,11 +12,15 @@
.col-sm-10
.input-group.gl-field-error-anchor
.input-group-addon
- = root_url
+ %span>= root_url
+ - if parent
+ %strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE,
title: 'Please choose a group name with no special characters.'
+ - if parent
+ = f.hidden_field :parent_id, value: parent.id
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 65a3a6bddab..54b5ae2402e 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
.well.prepend-top-20
%ul
@@ -13,4 +13,4 @@
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
- To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
+ To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
new file mode 100644
index 00000000000..1264e524d86
--- /dev/null
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -0,0 +1,19 @@
+- note_count = @issuable_meta_data[issuable.id].notes_count
+- issue_votes = @issuable_meta_data[issuable.id]
+- upvotes, downvotes = issue_votes.upvotes, issue_votes.downvotes
+- issuable_url = @collection_type == "Issue" ? issue_path(issuable, anchor: 'notes') : merge_request_path(issuable, anchor: 'notes')
+
+- if upvotes > 0
+ %li
+ = icon('thumbs-up')
+ = upvotes
+
+- if downvotes > 0
+ %li
+ = icon('thumbs-down')
+ = downvotes
+
+%li
+ = link_to issuable_url, class: ('no-comments' if note_count.zero?) do
+ = icon('comments')
+ = note_count
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 26b349e8a62..3a49227961f 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,16 +1,7 @@
- if @issues.to_a.any?
- - @issues.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project)
- - if can?(current_user, :create_issue, project)
- .pull-right
- = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
-
- %ul.content-list.issues-list
- - group[1].each do |issue|
- = render 'projects/issues/issue', issue: issue
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.issues-list
+ = render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
= render 'shared/empty_states/issues'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index f11f4471a9d..ead9b84b991 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -36,7 +36,7 @@
%li
= link_to 'Edit', edit_label_path(label)
%li
- = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
+ = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
@@ -66,11 +66,15 @@
%a.js-subscribe-button{ data: { url: toggle_subscription_group_label_path(label.group, label) } }
Group level
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_group, label.project.group)
+ = link_to promote_namespace_project_label_path(label.project.namespace, label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "Promoting this label will make this label available to all projects inside this group. Existing project labels with the same name will be merged. Are you sure?", toggle: "tooltip"}, method: :post do
+ %span.sr-only Promote to Group
+ = icon('level-up')
- 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
%span.sr-only Edit
= icon('pencil-square-o')
- = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, remote: true, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
+ = link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index 2f3605b4d27..b7982b7fe9b 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -1,16 +1,8 @@
- if @merge_requests.to_a.any?
- - @merge_requests.group_by(&:target_project).each do |group|
- .panel.panel-default.panel-small
- - project = group[0]
- .panel-heading
- = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project)
- - if can?(current_user, :create_merge_request, project)
- .pull-right
- = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.mr-list
+ = render partial: 'projects/merge_requests/merge_request', collection: @merge_requests
- %ul.content-list.mr-list
- - group[1].each do |merge_request|
- = render 'projects/merge_requests/merge_request', merge_request: merge_request
= paginate @merge_requests, theme: "gitlab"
- else
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
new file mode 100644
index 00000000000..b0778653d4e
--- /dev/null
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -0,0 +1,18 @@
+.stage-cell
+ - pipeline.stages.each do |stage|
+ - if stage.status
+ - detailed_status = stage.detailed_status(current_user)
+ - icon_status = "#{detailed_status.icon}_borderless"
+ - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
+
+ .stage-container.dropdown{ class: klass }
+ %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
+ = custom_icon(icon_status)
+ = icon('caret-down')
+
+ %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
+ .arrow-up
+ .js-builds-dropdown-list.scrollable-menu
+
+ .js-builds-dropdown-loading.builds-dropdown-loading.hidden
+ %span.fa.fa-spinner.fa-spin
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index 0eba1fe075f..c06d1ffa59b 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,8 +1,7 @@
- if outdated_browser?
- - link = "https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/requirements.md#supported-web-browsers"
.browser-alert
GitLab may not work properly because you are using an outdated web browser.
%br
Please install a
- = link_to 'supported web browser', link
+ = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
for a better experience.
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index b11257ee0e6..73efec88bb1 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,8 +1,11 @@
+- with_label = local_assigns.fetch(:with_label, true)
+
.form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'control-label' do
- Visibility Level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- .col-sm-10
+ - if with_label
+ = f.label :visibility_level, class: 'control-label' do
+ Visibility Level
+ = link_to icon('question-circle'), help_page_path("public_access/public_access")
+ %div{ :class => ("col-sm-10" if with_label) }
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
new file mode 100644
index 00000000000..ba5c2dae09d
--- /dev/null
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -0,0 +1,11 @@
+.row.empty-state.labels
+ .pull-right.col-xs-12.col-sm-6
+ .svg-content
+ = render 'shared/empty_states/icons/labels.svg'
+ .col-xs-12.col-sm-6
+ .text-content
+ %h4 Labels can be applied to issues and merge requests to categorize them.
+ %p You can also star label to make it a priority label.
+ - if can?(current_user, :admin_label, @project)
+ = link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
+ = link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
new file mode 100644
index 00000000000..bc268301a97
--- /dev/null
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -0,0 +1,3 @@
+.text-center
+ = render 'shared/empty_states/icons/priority_labels.svg'
+ %p Star labels to start sorting by priority
diff --git a/app/views/shared/empty_states/icons/_labels.svg b/app/views/shared/empty_states/icons/_labels.svg
new file mode 100644
index 00000000000..dc041a4a78b
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="787 240 386 274" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><circle id="a" cx="37" cy="107" r="8"/><mask id="e" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><circle id="b" cx="37" cy="75" r="8"/><mask id="f" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><circle id="c" cx="42" cy="93" r="8"/><mask id="g" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask><circle id="d" cx="43" cy="75" r="8"/><mask id="h" width="16" height="16" x="0" y="0" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(791 244)"><g transform="rotate(30 49.554 229.722)"><rect width="74" height="124" x="8.6" y="95.9" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="87" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><circle cx="26.5" cy="178.5" r="3.5" fill="#FC8A51"/><circle cx="47.5" cy="178.5" r="3.5" fill="#FC8A51"/><rect width="50" height="4" x="12" y="127" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="139" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#e)" stroke-linecap="round" xlink:href="#a"/><path stroke="#EEE" stroke-width="4" d="M37.3 107S10.5 18.3 81 .6" stroke-linecap="round"/><path fill="#FDE5D8" d="M31 189c0 3.3 2.7 6 6 6s6-2.7 6-6"/></g><g transform="translate(105 47)"><rect width="74" height="124" y="64" fill="#FAFAFA" rx="8"/><rect width="74" height="124" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><rect width="50" height="4" x="12" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="18" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#f)" stroke-linecap="round" xlink:href="#b"/><path fill="#B5A7DD" d="M56 149.7c-.6-1-.2-2 .7-2.7l1.8-1c1-.6 2-.2 2.7.7.5 1 .2 2.2-.7 2.8l-1.8 1c-1 .5-2 .2-2.7-.8zm-37.8 0c.5-1 .2-2-.7-2.7l-1.8-1c-1-.6-2-.2-2.7.7-.6 1-.2 2.2.7 2.8l1.8 1c1 .5 2 .2 2.7-.8zM33 151h9v4h-9v-4z"/><path fill="#6B4FBB" d="M59 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6zM35 153c0-5.5-4.6-10-10-10-5.7 0-10 4.5-10 10s4.3 10 10 10c5.4 0 10-4.5 10-10zm-16 0c0-3.3 2.6-6 6-6 3.2 0 6 2.7 6 6s-2.8 6-6 6c-3.4 0-6-2.7-6-6z"/><path stroke="#EEE" stroke-width="4" d="M37 75S30 0 80 0" stroke-linecap="round"/></g><g transform="rotate(15 -82.507 752.644)"><rect width="74" height="124" x="14.6" y="81.8" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="5" y="73" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><path fill="#FDE5D8" d="M41 147c0-1 1-2 2-2s2 1 2 2v3c0 1-1 2-2 2s-2-1-2-2v-3zm16.8 6.2c.8-.7 2-.6 2.8.3.7.8.5 2-.3 2.8L58 158c-1 .8-2.2.7-3 0-.6-1-.4-2.3.4-3l2.4-1.8zm-32 3c-1-.6-1-2-.4-2.7.7-1 2-1 2.8-.3l2.4 1.8c.8.7 1 2 .3 3-.8.7-2 1-3 0l-2.3-1.7z"/><rect width="2" height="7" x="39" y="168" fill="#FC8A51" rx="1"/><rect width="2" height="7" x="45" y="168" fill="#FC8A51" rx="1"/><circle cx="40" cy="169" r="2" fill="#FC8A51"/><circle cx="46" cy="169" r="2" fill="#FC8A51"/><rect width="22" height="18" x="32" y="158" stroke="#FC8A51" stroke-width="4" rx="8"/><rect width="34" height="5" x="26" y="174" fill="#FC8A51" rx="2.5"/><rect width="50" height="4" x="17" y="113" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="23" y="125" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#g)" stroke-linecap="round" xlink:href="#c"/><path stroke="#EEE" stroke-width="4" d="M42 93S50 0 0 0" stroke-linecap="round"/></g><g transform="rotate(-15 276.18 -697.744)"><rect width="74" height="124" x="18.7" y="65.6" fill="#FAFAFA" rx="8"/><rect width="74" height="124" x="6" y="55" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="8"/><g transform="translate(25 129)"><path stroke="#B5A7DD" stroke-width="4" d="M32 14c0-7.7-6.3-14-14-14S4 6.3 4 14" stroke-linecap="round"/><path stroke="#B5A7DD" stroke-width="2" d="M33 15v13c0 4.4-3.6 8-8 8" stroke-linecap="round"/><rect width="7" height="4" x="20" y="34" fill="#6B4FBB" rx="2"/><rect width="7" height="13" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" rx="3.5"/><rect width="7" height="13" x="29" y="15" fill="#FFF" stroke="#6B4FBB" stroke-width="3" stroke-linejoin="round" transform="matrix(-1 0 0 1 65 0)" rx="3.5"/></g><rect width="50" height="4" x="18" y="95" fill="#E5E5E5" rx="2"/><rect width="38" height="4" x="24" y="107" fill="#E5E5E5" rx="2"/><use stroke="#E5E5E5" stroke-width="8" mask="url(#h)" stroke-linecap="round" xlink:href="#d"/><path stroke="#EEE" stroke-width="4" d="M43 75S50 0 0 0" stroke-linecap="round"/></g><circle cx="193" cy="47" r="12" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><circle cx="193" cy="47" r="5" fill="#FFF" stroke="#FDE5D8" stroke-width="4"/><g opacity=".2"><path fill="#FC8A51" d="M30.7 254.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM374.7 133.8l-2.6 1c-1 .5-1.7 0-1.7-1v-3l-1-2.7c-.4-1 .2-1.7 1.2-1.7h3l2.6-1c1.2-.4 2 .2 2 1.2l-.2 3 1 2.6c.5 1.2 0 2-1 2l-3-.2zM5.6 95H1.8c-1.3.2-2-.8-1.4-2l1.4-3.4-.2-3.8c0-1.3 1-2 2-1.4l3.6 1.4 3.7-.2c1.2 0 2 1 1.4 2L11 91.3V95c.2 1.2-.8 2-2 1.4L5.6 95z"/><path fill="#6B4FBB" d="M308.8 62l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8zM318 226.6h-3c-1-.2-1.4-1-1-2l1.4-2.5v-3c.2-1 1-1.4 2-1l2.6 1.4h3c1 .2 1.5 1 1 2l-1.4 2.6v3c-.2 1-1 1.5-2 1l-2.5-1.4zM121.8 8l-2-2.3c-.7-.8-.5-1.7.6-2l2.8-1 2-2c1-.6 1.8-.4 2.2.7l.8 2.8 2 2c.8 1 .5 1.8-.5 2.2l-2.8.8-2.3 2c-.8.8-1.7.5-2-.5l-1-2.8z"/></g></g></svg>
diff --git a/app/views/shared/empty_states/icons/_priority_labels.svg b/app/views/shared/empty_states/icons/_priority_labels.svg
new file mode 100644
index 00000000000..7282c2b215e
--- /dev/null
+++ b/app/views/shared/empty_states/icons/_priority_labels.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="116" height="68" viewBox="181 0 116 68"><g fill="none" fill-rule="evenodd" transform="translate(182)"><rect width="78" height="34" x="37" y="34" fill="#FAFAFA" rx="3"/><rect width="78" height="34" x="31" y="28" fill="#FFF" stroke="#EEE" stroke-width="4" stroke-linecap="round" rx="3"/><path fill="#FFF" stroke="#FC6D26" stroke-width="3" d="M34 35.8c-.6 0-1.4 0-1.8.4L29 38.8c-1 .7-1.7.4-2-.7l-.6-4c0-.5-.5-1.2-1-1.5L22 30.2c-1-.6-1-1.5 0-2l3.7-2c.5-.2 1-.8 1.2-1.3l1-4.2c.3-1 1-1.3 2-.5l3 3c.3.3 1 .6 1.6.6l4.2-.3c1 0 1.5.7 1 1.7L38 29c-.3.6-.3 1.4 0 2l1.3 3.8c.4 1 0 1.8-1.2 1.6l-4-.6z" stroke-linecap="round"/><path fill="#FDE5D8" d="M51.6 14.3c-.2-.2-.8-.4-1-.3l-2.8.5c-.7 0-1-.4-.8-1l1-2.8V9.5L46.6 7c-.3-.7 0-1.2.8-1h2.7c.3 0 .8-.2 1-.5l2-2c.6-.5 1-.4 1.3.3l.7 2.8c0 .3.4.8.7 1l2.3 1.2c.7.3.7 1 0 1.3l-2.2 1.7-.6 1-.4 3c-.2.6-.7.8-1.3.4l-2-1.7zM5.4 43.2c-.2-.2-.5-.2-.7-.2l-1.8.3c-.6 0-1-.2-.7-.7l.7-1.8V40l-1-1.7c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L6.5 36c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2zM10.4 9.2C10.2 9 10 9 9.7 9L8 9.3c-.6 0-1-.2-.7-.7L8 6.8V6L7 4.3c0-.4 0-.7.6-.7h1.8c.3 0 .6 0 .8-.2L11.5 2c.3-.3.7-.2.8.2l.5 2 .5.5 1.6.8c.3.2.3.7 0 1l-1.6 1c-.2 0-.4.4-.4.7l-.4 2c0 .3-.4.5-.8.2l-1.4-1.2z"/><rect width="52" height="4" x="43" y="38" fill="#EEE" rx="2"/><rect width="36" height="4" x="43" y="48" fill="#EEE" rx="2"/></g></svg>
diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/icons/_todos_all_done.svg
index 94b5c2e0ea0..94b5c2e0ea0 100644
--- a/app/views/shared/empty_states/_todos_all_done.svg
+++ b/app/views/shared/empty_states/icons/_todos_all_done.svg
diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/icons/_todos_empty.svg
index b1e661268fb..b1e661268fb 100644
--- a/app/views/shared/empty_states/_todos_empty.svg
+++ b/app/views/shared/empty_states/icons/_todos_empty.svg
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index dd9e433491b..60ca23ef680 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,4 +1,5 @@
- group_member = local_assigns[:group_member]
+- full_name = true unless local_assigns[:full_name] == false
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
@@ -28,7 +29,10 @@
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
- = group.full_name
+ - if full_name
+ = group.full_name
+ - else
+ = group.name
- if group_member
as
diff --git a/app/views/shared/icons/_icon_action_cancel.svg b/app/views/shared/icons/_icon_action_cancel.svg
new file mode 100644
index 00000000000..a1f700eb0ff
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_cancel.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M19.25,14.9765625 C19.25,14.1380166 19.0234398,13.3697952 18.5703125,12.671875 L12.6796875,18.5546875 C13.3932327,19.0182315 14.1666625,19.25 15,19.25 C15.5781279,19.25 16.1289036,19.1367199 16.6523438,18.9101562 C17.1757839,18.6835926 17.6276023,18.3802102 18.0078125,18 C18.3880227,17.6197898 18.690103,17.1653672 18.9140625,16.6367188 C19.138022,16.1080703 19.25,15.5546904 19.25,14.9765625 Z M11.4453125,17.3125 L17.34375,11.421875 C16.6406215,10.9479143 15.8593793,10.7109375 15,10.7109375 C14.2291628,10.7109375 13.5182324,10.9010398 12.8671875,11.28125 C12.2161426,11.6614602 11.7005227,12.1796842 11.3203125,12.8359375 C10.9401023,13.4921908 10.75,14.2057253 10.75,14.9765625 C10.75,15.8203167 10.9817685,16.5989548 11.4453125,17.3125 Z M21,14.9765625 C21,15.7942749 20.8411474,16.5755171 20.5234375,17.3203125 C20.2057276,18.0651079 19.7799506,18.7057265 19.2460938,19.2421875 C18.7122369,19.7786485 18.0742225,20.2057276 17.3320312,20.5234375 C16.58984,20.8411474 15.8125041,21 15,21 C14.1874959,21 13.41016,20.8411474 12.6679688,20.5234375 C11.9257775,20.2057276 11.2877631,19.7786485 10.7539062,19.2421875 C10.2200494,18.7057265 9.79427242,18.0651079 9.4765625,17.3203125 C9.15885258,16.5755171 9,15.7942749 9,14.9765625 C9,14.1588501 9.15885258,13.37891 9.4765625,12.6367188 C9.79427242,11.8945275 10.2200494,11.255211 10.7539062,10.71875 C11.2877631,10.182289 11.9257775,9.75520992 12.6679688,9.4375 C13.41016,9.11979008 14.1874959,8.9609375 15,8.9609375 C15.8125041,8.9609375 16.58984,9.11979008 17.3320312,9.4375 C18.0742225,9.75520992 18.7122369,10.182289 19.2460938,10.71875 C19.7799506,11.255211 20.2057276,11.8945275 20.5234375,12.6367188 C20.8411474,13.37891 21,14.1588501 21,14.9765625 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_play.svg b/app/views/shared/icons/_icon_action_play.svg
new file mode 100644
index 00000000000..6ac192cd7e9
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_play.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M21.5401786,15.2320328 L11.90625,20.5858274 C11.7950143,20.6486998 11.6994982,20.6559541 11.6196987,20.6075908 C11.5398992,20.5592275 11.5,20.4721748 11.5,20.3464301 L11.5,9.66785867 C11.5,9.54211399 11.5398992,9.45506129 11.6196987,9.40669795 C11.6994982,9.35833462 11.7950143,9.36558901 11.90625,9.42846135 L21.5401786,14.782256 C21.6514142,14.8451283 21.7070312,14.9200904 21.7070312,15.0071444 C21.7070312,15.0941984 21.6514142,15.1691604 21.5401786,15.2320328 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_retry.svg b/app/views/shared/icons/_icon_action_retry.svg
new file mode 100644
index 00000000000..0fa0243f3c0
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_retry.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg"><path d="M20.6114971,16.0821413 C20.6114971,16.106323 20.6090789,16.1232499 20.6042426,16.1329226 C20.2947172,17.42906 19.6466582,18.4797378 18.6600462,19.2849873 C17.6734341,20.0902369 16.5175677,20.4928556 15.1924122,20.4928556 C14.4863075,20.4928556 13.8031856,20.3598584 13.1430261,20.0938601 C12.4828665,19.8278617 11.8940517,19.4482152 11.376564,18.9549092 L10.4407381,19.8907351 C10.3488478,19.9826254 10.2400319,20.0285699 10.1142872,20.0285699 C9.98854256,20.0285699 9.87972669,19.9826254 9.78783635,19.8907351 C9.69594601,19.7988447 9.65000153,19.6900289 9.65000153,19.5642842 L9.65000153,16.3142842 C9.65000153,16.1885395 9.69594601,16.0797236 9.78783635,15.9878333 C9.87972669,15.895943 9.98854256,15.8499985 10.1142872,15.8499985 L13.3642872,15.8499985 C13.4900319,15.8499985 13.5988478,15.895943 13.6907381,15.9878333 C13.7826285,16.0797236 13.828573,16.1885395 13.828573,16.3142842 C13.828573,16.4400289 13.7826285,16.5488447 13.6907381,16.6407351 L12.6968765,17.6345967 C13.0402562,17.9537947 13.4295752,18.200444 13.8648453,18.374552 C14.3001153,18.5486601 14.7523057,18.6357128 15.2214301,18.6357128 C15.8694988,18.6357128 16.4740315,18.4785343 17.0350462,18.1641726 C17.5960609,17.8498109 18.0458332,17.4169655 18.3843765,16.8656235 C18.4375762,16.7834058 18.5657371,16.5004845 18.7688631,16.0168512 C18.8075538,15.9056155 18.8800977,15.8499985 18.9864971,15.8499985 L20.3793542,15.8499985 C20.4422265,15.8499985 20.4966345,15.8729707 20.5425797,15.9189159 C20.5885248,15.9648611 20.6114971,16.019269 20.6114971,16.0821413 Z M20.7928587,10.2785699 L20.7928587,13.5285699 C20.7928587,13.6543146 20.7469142,13.7631305 20.6550238,13.8550208 C20.5631335,13.9469111 20.4543176,13.9928556 20.328573,13.9928556 L17.078573,13.9928556 C16.9528283,13.9928556 16.8440124,13.9469111 16.7521221,13.8550208 C16.6602317,13.7631305 16.6142872,13.6543146 16.6142872,13.5285699 C16.6142872,13.4028252 16.6602317,13.2940094 16.7521221,13.202119 L17.7532381,12.2010029 C17.0374607,11.5384252 16.1935332,11.2071413 15.2214301,11.2071413 C14.5733614,11.2071413 13.9688287,11.3643198 13.407814,11.6786815 C12.8467993,11.9930432 12.397027,12.4258886 12.0584837,12.9772306 C12.005284,13.0594483 11.8771231,13.3423696 11.6739971,13.8260029 C11.6353064,13.9372386 11.5627625,13.9928556 11.4563631,13.9928556 L10.0127247,13.9928556 C9.9498524,13.9928556 9.89544446,13.9698834 9.84949929,13.9239382 C9.80355412,13.877993 9.78058188,13.8235851 9.78058188,13.7607128 L9.78058188,13.7099315 C10.0949436,12.4137941 10.7478388,11.3631163 11.7392872,10.5578668 C12.7307356,9.75261722 13.8914383,9.34999847 15.2214301,9.34999847 C15.9275348,9.34999847 16.6142839,9.48420472 17.281698,9.75262124 C17.949112,10.0210378 18.541554,10.3994752 19.0590417,10.8879449 L20.0021221,9.95211901 C20.0940124,9.86022867 20.2028283,9.81428419 20.328573,9.81428419 C20.4543176,9.81428419 20.5631335,9.86022867 20.6550238,9.95211901 C20.7469142,10.0440094 20.7928587,10.1528252 20.7928587,10.2785699 Z"></path></svg>
diff --git a/app/views/shared/icons/_icon_action_stop.svg b/app/views/shared/icons/_icon_action_stop.svg
new file mode 100644
index 00000000000..1c8e2fe4156
--- /dev/null
+++ b/app/views/shared/icons/_icon_action_stop.svg
@@ -0,0 +1 @@
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M20.1357204,10.2985704 L20.1357204,19.7271418 C20.1357204,19.8432138 20.0933101,19.9436592 20.0084882,20.0284811 C19.9236664,20.1133029 19.823221,20.1557132 19.707149,20.1557132 L10.2785775,20.1557132 C10.1625055,20.1557132 10.0620601,20.1133029 9.97723825,20.0284811 C9.89241639,19.9436592 9.8500061,19.8432138 9.8500061,19.7271418 L9.8500061,10.2985704 C9.8500061,10.1824984 9.89241639,10.0820529 9.97723825,9.99723107 C10.0620601,9.91240922 10.1625055,9.86999893 10.2785775,9.86999893 L19.707149,9.86999893 C19.823221,9.86999893 19.9236664,9.91240922 20.0084882,9.99723107 C20.0933101,10.0820529 20.1357204,10.1824984 20.1357204,10.2985704 Z"></path></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index b42eaabb111..f17ae9f28eb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -38,8 +38,9 @@
#js-boards-search.issue-boards-search
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
+ #js-add-issues-btn.pull-right.prepend-left-10
.dropdown.pull-right
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
@@ -53,7 +54,7 @@
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
- = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+ = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
@@ -61,13 +62,13 @@
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
+ placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
- = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
+ = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
- = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
@@ -91,5 +92,5 @@
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
- Turbolinks.visit(this.action + '&' + $(this).serialize());
+ gl.utils.visitUrl(this.action + '&' + $(this).serialize());
});
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0a4de709fcd..cb92b2e97a7 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -43,6 +43,8 @@
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
+= render 'shared/issuable/form/merge_params', issuable: issuable
+
- if @merge_request_for_resolving_discussions
.form-group
.col-sm-10.col-sm-offset-2
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 55360dadbc4..6e417aa2251 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,7 +11,7 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) }
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
@@ -101,7 +101,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index ec9bcaf63dd..3f7f1a86b9f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,7 +1,8 @@
- todo = issuable_todo(issuable)
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('issuable/issuable_bundle.js')
-%aside.right-sidebar{ class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+ = page_specific_javascript_bundle_tag('issuable')
+
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -130,7 +131,7 @@
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
- = link_to_label(label, type: issuable.to_ability_name)
+ = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name)
- else
%span.no-value None
.selectbox.hide-collapsed
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index b757893ea04..2793e7bcff4 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -19,12 +19,3 @@
- if issuable.new_record?
&nbsp;
= link_to 'Change branches', mr_change_branches_path(issuable)
-
-- if issuable.can_remove_source_branch?(current_user)
- .form-group
- .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', id: nil
- = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
- Remove source branch when merge request is accepted.
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
new file mode 100644
index 00000000000..03309722326
--- /dev/null
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -0,0 +1,16 @@
+- issuable = local_assigns.fetch(:issuable)
+
+- return unless issuable.is_a?(MergeRequest)
+- return if issuable.closed_without_fork?
+
+-# This check is duplicated below, to avoid conflicts with EE.
+- return unless issuable.can_remove_source_branch?(current_user)
+
+.form-group
+ .col-sm-10.col-sm-offset-2
+ - if issuable.can_remove_source_branch?(current_user)
+ .checkbox
+ = label_tag 'merge_request[force_remove_source_branch]' do
+ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
+ = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
+ Remove source branch when merge request is accepted.
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 659d4c905fc..239387fc9fa 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -19,9 +19,9 @@
%label.label.label-danger
%strong Blocked
- - if source.instance_of?(Group) && !@group
+ - if source.instance_of?(Group) && source != @group
&middot;
- = link_to source.name, source, class: "member-group-link"
+ = link_to source.full_name, source, class: "member-group-link"
.hidden-xs.cgray
- if member.request?
@@ -44,8 +44,9 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- if show_roles
+ - current_resource = @project || @group
.controls.member-controls
- - if show_controls && (member.respond_to?(:group) && @group) || (member.respond_to?(:project) && @project)
+ - if show_controls && member.source == current_resource
- if user != current_user
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 748b10a1298..ed94773ef89 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -10,6 +10,3 @@
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
-
-:javascript
- new gl.DueDateSelectors();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 28935c8b598..4c7d69d40d5 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -5,7 +5,7 @@
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
-%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'is-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-id' => issuable.id, 'data-url' => polymorphic_path(issuable) }
%span
- if show_project_name
%strong #{project.name} &middot;
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index b5c0a7fd6d4..a736bfd91e2 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -18,7 +18,7 @@
%p
Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
= succeed "." do
- %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank" } notification emails
+ %a{ href: help_page_path('workflow/notifications'), target: "_blank" } notification emails
.col-lg-8
- NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
- field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index b7f8551153b..c19697802ce 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,8 +1,9 @@
- @sort ||= sort_value_recently_updated
- personal = params[:personal]
- archived = params[:archived]
+- shared = params[:shared]
- namespace_id = params[:namespace_id]
-.dropdown.inline
+.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
@@ -28,3 +29,14 @@
%li
= link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, personal: true), class: ("is-active" if personal.present?) do
Owned by me
+ - if @group && @group.shared_projects.present?
+ %li.divider
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: nil), class: ("is-active" unless shared.present?) do
+ All projects
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 0), class: ("is-active" if shared == '0') do
+ Hide shared projects
+ %li
+ = link_to filter_projects_path(namespace_id: namespace_id, sort: @sort, shared: 1), class: ("is-active" if shared == '1') do
+ Hide group projects
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 0c788032020..e7f7db73223 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = page_specific_javascript_tag('snippet/snippet_bundle.js')
+ = page_specific_javascript_bundle_tag('snippet')
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
@@ -11,14 +11,14 @@
.col-sm-10
= f.text_field :title, class: 'form-control', required: true, autofocus: true
- = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
+ = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet
.file-editor
.form-group
= f.label :file_name, "File", class: 'control-label'
.col-sm-10
.file-holder.snippet
- .file-title
+ .js-file-title.file-title
= f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name'
.file-content.code
%pre#editor= @snippet.content
@@ -34,4 +34,3 @@
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index 13586a5a12a..37e2a377a69 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -3,7 +3,7 @@
%h4.prepend-top-0
= page_title
%p
- #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be
+ #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
@@ -66,9 +66,9 @@
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
- %strong Build events
+ %strong Jobs events
%p.light
- This URL will be triggered when the build status changes
+ This URL will be triggered when the job status changes
%li
= f.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
index 92151176fce..1a6e2542dc1 100644
--- a/app/views/sherlock/file_samples/show.html.haml
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -26,7 +26,7 @@
= @file_sample.events
%article.file-holder
- .file-title
+ .js-file-title.file-title
%i.fa.fa-file-text-o.fa-fw
%strong
= @file_sample.file
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index 95fc7198104..855a995afa9 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,3 +1,5 @@
+- return unless current_user
+
.hidden-xs
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
@@ -5,24 +7,27 @@
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- - if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
- New snippet
-- if current_user
- .visible-xs-block.dropdown
- %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-full-width
- %ul
+ = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
+ New snippet
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
+.visible-xs-block.dropdown
+ %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-full-width
+ %ul
+ %li
+ = link_to new_snippet_path, title: "New snippet" do
+ New snippet
+ - if can?(current_user, :admin_personal_snippet, @snippet)
%li
- = link_to new_snippet_path, title: "New snippet" do
- New snippet
- - if can?(current_user, :admin_personal_snippet, @snippet)
- %li
- = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
- Delete
- - if can?(current_user, :update_personal_snippet, @snippet)
- %li
- = link_to edit_snippet_path(@snippet) do
- Edit
+ = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
+ Delete
+ - if can?(current_user, :update_personal_snippet, @snippet)
+ %li
+ = link_to edit_snippet_path(@snippet) do
+ Edit
+ - if @snippet.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 82f44a9a5c3..915bf98eb3e 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
Edit Snippet
%hr
-= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
+= render 'shared/snippets/form', url: snippet_path(@snippet)
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 79e2392490d..ca8afb4bb6a 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -2,4 +2,4 @@
%h3.page-title
New Snippet
%hr
-= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
+= render "shared/snippets/form", url: snippets_path(@snippet)
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 837a1a0cc8c..970afbe6b64 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,7 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- .file-title
+ .js-file-title.file-title
= blob_icon 0, @snippet.file_name
= @snippet.file_name
.file-actions
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c3d33d49c1e..44254040e4e 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,8 +1,8 @@
- page_title @user.name
- page_description @user.bio
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/d3.js')
- = page_specific_javascript_tag('users/users_bundle.js')
+ = page_specific_javascript_bundle_tag('lib_d3')
+ = page_specific_javascript_bundle_tag('users')
- header_title @user.name, user_path(@user)
- @no_container = true
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 6abbb5a5250..0e20df506a3 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -16,6 +16,6 @@ class AuthorizedProjectsWorker
def perform(user_id)
user = User.find_by(id: user_id)
- user.refresh_authorized_projects if user
+ user&.refresh_authorized_projects
end
end
diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb
index 3194c389b3d..5483bbb210b 100644
--- a/app/workers/delete_user_worker.rb
+++ b/app/workers/delete_user_worker.rb
@@ -6,6 +6,6 @@ class DeleteUserWorker
delete_user = User.find(delete_user_id)
current_user = User.find(current_user_id)
- DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys)
+ Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index b9cd49985dc..f5ccc84c160 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,15 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new(project, after_sha)
+ .execute(project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new(project, before_sha)
+ .execute(project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index a49a5fd0855..07e82767b06 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -11,6 +11,6 @@ class GroupDestroyWorker
user = User.find(user_id)
- DestroyGroupService.new(group, user).execute
+ Groups::DestroyService.new(group, user).execute
end
end
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
new file mode 100644
index 00000000000..4eeb9666bb0
--- /dev/null
+++ b/app/workers/pages_worker.rb
@@ -0,0 +1,23 @@
+class PagesWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :pages, retry: false
+
+ def perform(action, *arg)
+ send(action, *arg)
+ end
+
+ def deploy(build_id)
+ build = Ci::Build.find_by(id: build_id)
+ result = Projects::UpdatePagesService.new(build.project, build).execute
+ if result[:status] == :success
+ result = Projects::UpdatePagesConfigurationService.new(build.project).execute
+ end
+ result
+ end
+
+ def remove(namespace_path, project_path)
+ full_path = File.join(Settings.pages.path, namespace_path, project_path)
+ FileUtils.rm_r(full_path, force: true)
+ end
+end
diff --git a/bin/teaspoon b/bin/teaspoon
deleted file mode 100755
index 7c3b8dfc4ed..00000000000
--- a/bin/teaspoon
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env ruby
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
-end
-require 'bundler/setup'
-load Gem.bin_path('teaspoon', 'teaspoon')
diff --git a/changelogs/unreleased/1051-api-create-users-without-password.yml b/changelogs/unreleased/1051-api-create-users-without-password.yml
new file mode 100644
index 00000000000..24b5a73b45c
--- /dev/null
+++ b/changelogs/unreleased/1051-api-create-users-without-password.yml
@@ -0,0 +1,4 @@
+---
+title: Optionally make users created via the API set their password
+merge_request: 8957
+author: Joost Rijneveld
diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml
new file mode 100644
index 00000000000..12f2998d1c8
--- /dev/null
+++ b/changelogs/unreleased/17662-rename-builds.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere
+merge_request: 8787
+author:
diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
new file mode 100644
index 00000000000..965d0648adf
--- /dev/null
+++ b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml
@@ -0,0 +1,4 @@
+---
+title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb
+merge_request:
+author:
diff --git a/changelogs/unreleased/20495-plus-icon-button.yml b/changelogs/unreleased/20495-plus-icon-button.yml
new file mode 100644
index 00000000000..0f8650eb7b6
--- /dev/null
+++ b/changelogs/unreleased/20495-plus-icon-button.yml
@@ -0,0 +1,4 @@
+---
+title: Remove plus icon from MR button on compare view
+merge_request:
+author:
diff --git a/changelogs/unreleased/20732_member_exists_409.yml b/changelogs/unreleased/20732_member_exists_409.yml
new file mode 100644
index 00000000000..135647c7ac3
--- /dev/null
+++ b/changelogs/unreleased/20732_member_exists_409.yml
@@ -0,0 +1,4 @@
+---
+title: 'Add member: Always return 409 when a member exists'
+merge_request:
+author:
diff --git a/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
new file mode 100644
index 00000000000..eda872049fd
--- /dev/null
+++ b/changelogs/unreleased/20852-getting-started-project-better-blank-state-for-labels-view.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels empty state
+merge_request: 7443
+author:
diff --git a/changelogs/unreleased/21518_recaptcha_spam_issues.yml b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
new file mode 100644
index 00000000000..bd6c9d7521e
--- /dev/null
+++ b/changelogs/unreleased/21518_recaptcha_spam_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Use reCaptcha when an issue is identified as a spam
+merge_request: 8846
+author:
diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml
new file mode 100644
index 00000000000..f43c1925ad0
--- /dev/null
+++ b/changelogs/unreleased/22007-unify-projects-search.yml
@@ -0,0 +1,4 @@
+---
+title: Unify projects search by removing /projects/:search endpoint
+merge_request: 8877
+author:
diff --git a/changelogs/unreleased/23104-remove-public-param-for-projects.yml b/changelogs/unreleased/23104-remove-public-param-for-projects.yml
new file mode 100644
index 00000000000..78eb785279f
--- /dev/null
+++ b/changelogs/unreleased/23104-remove-public-param-for-projects.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: remove `public` param for projects'
+merge_request: 8736
+author:
diff --git a/changelogs/unreleased/23634-remove-project-grouping.yml b/changelogs/unreleased/23634-remove-project-grouping.yml
new file mode 100644
index 00000000000..dde8b2d1815
--- /dev/null
+++ b/changelogs/unreleased/23634-remove-project-grouping.yml
@@ -0,0 +1,4 @@
+---
+title: Don't group issues by project on group-level and dashboard issue indexes.
+merge_request: 8111
+author: Bernardo Castro
diff --git a/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
new file mode 100644
index 00000000000..587ef4f9a73
--- /dev/null
+++ b/changelogs/unreleased/23767-disable-storing-of-sensitive-information.yml
@@ -0,0 +1,4 @@
+---
+title: Fix disable storing of sensitive information when importing a new repo
+merge_request: 8885
+author: Bernard Pietraga
diff --git a/changelogs/unreleased/24147-delete-env-button.yml b/changelogs/unreleased/24147-delete-env-button.yml
new file mode 100644
index 00000000000..14e80cacbfb
--- /dev/null
+++ b/changelogs/unreleased/24147-delete-env-button.yml
@@ -0,0 +1,4 @@
+---
+title: Adds back ability to stop all environments
+merge_request: 7379
+author:
diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
new file mode 100644
index 00000000000..05fbd8f0bf2
--- /dev/null
+++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml
@@ -0,0 +1,4 @@
+---
+title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms
+merge_request: 8752
+author:
diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
new file mode 100644
index 00000000000..fd671d04a9f
--- /dev/null
+++ b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml
@@ -0,0 +1,4 @@
+---
+title: Force new password after password reset via API
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
new file mode 100644
index 00000000000..13de5db5e41
--- /dev/null
+++ b/changelogs/unreleased/24716-fix-ctrl-click-links.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Ctrl+Click support for Todos and Merge Request page tabs
+merge_request: 8898
+author:
diff --git a/changelogs/unreleased/24795_refactor_merge_request_build_service.yml b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
new file mode 100644
index 00000000000..b735fb57649
--- /dev/null
+++ b/changelogs/unreleased/24795_refactor_merge_request_build_service.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor MergeRequests::BuildService
+merge_request: 8462
+author: Rydkin Maxim
diff --git a/changelogs/unreleased/24976-start-of-line-mention.yml b/changelogs/unreleased/24976-start-of-line-mention.yml
new file mode 100644
index 00000000000..99208aac87c
--- /dev/null
+++ b/changelogs/unreleased/24976-start-of-line-mention.yml
@@ -0,0 +1,4 @@
+---
+title: Added a feature to create a 'directly addressed' Todo when mentioned in the beginning of a line.
+merge_request: 7926
+author: Ershad Kunnakkadan
diff --git a/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
new file mode 100644
index 00000000000..d35ad0be0db
--- /dev/null
+++ b/changelogs/unreleased/25134-mobile-issue-view-doesn-t-show-organization-membership.yml
@@ -0,0 +1,4 @@
+---
+title: Show organisation membership and delete comment on smaller viewports, plus change comment author name to username
+merge_request:
+author:
diff --git a/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
new file mode 100644
index 00000000000..50a5c879446
--- /dev/null
+++ b/changelogs/unreleased/25360-remove-flash-warning-from-login-page.yml
@@ -0,0 +1,4 @@
+---
+title: Remove flash warning from login page
+merge_request: 8864
+author: Gerald J. Padilla
diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml
new file mode 100644
index 00000000000..dac90eaa34d
--- /dev/null
+++ b/changelogs/unreleased/25460-replace-word-users-with-members.yml
@@ -0,0 +1,4 @@
+---
+title: Replace word user with member
+merge_request: 8872
+author:
diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
new file mode 100644
index 00000000000..d7f950d7be9
--- /dev/null
+++ b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml
@@ -0,0 +1,4 @@
+---
+title: Remove turbolinks.
+merge_request: !8570
+author:
diff --git a/changelogs/unreleased/25709-diff-file-overflow.yml b/changelogs/unreleased/25709-diff-file-overflow.yml
new file mode 100644
index 00000000000..7d1b2b36ab8
--- /dev/null
+++ b/changelogs/unreleased/25709-diff-file-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Responsive title in diffs inline, side by side, with and without sidebar
+merge_request: 8475
+author:
diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
new file mode 100644
index 00000000000..f74e9fa8b6d
--- /dev/null
+++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml
@@ -0,0 +1,4 @@
+---
+title: Update pipeline and commit links when CI status is updated
+merge_request: 8351
+author:
diff --git a/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
new file mode 100644
index 00000000000..9506692dd40
--- /dev/null
+++ b/changelogs/unreleased/25910-convert-manual-action-icons-to-svg-to-propperly-position-them.yml
@@ -0,0 +1,4 @@
+---
+title: Convert pipeline action icons to svg to have them propperly positioned
+merge_request:
+author:
diff --git a/changelogs/unreleased/26059-segoe-ui-vertical.yml b/changelogs/unreleased/26059-segoe-ui-vertical.yml
new file mode 100644
index 00000000000..fc3f1af5b61
--- /dev/null
+++ b/changelogs/unreleased/26059-segoe-ui-vertical.yml
@@ -0,0 +1,4 @@
+---
+title: Align Segoe UI label text
+merge_request:
+author:
diff --git a/changelogs/unreleased/26705-filter-todos-by-manual-add.yml b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml
new file mode 100644
index 00000000000..3521496a20e
--- /dev/null
+++ b/changelogs/unreleased/26705-filter-todos-by-manual-add.yml
@@ -0,0 +1,4 @@
+---
+title: Filter todos by manual add
+merge_request: 8691
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml b/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml
deleted file mode 100644
index 2d4ec482ee0..00000000000
--- a/changelogs/unreleased/26775-fix-auto-complete-initial-loading.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix autocomplete initial undefined state
-merge_request:
-author:
diff --git a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml b/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml
deleted file mode 100644
index 4678297cfd4..00000000000
--- a/changelogs/unreleased/26844-new-search-bar-performs-a-new-request-for-each-tag.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add caching of droplab ajax requests
-merge_request: 8725
-author:
diff --git a/changelogs/unreleased/26852-fix-slug-for-openshift.yml b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
new file mode 100644
index 00000000000..fb65b068b23
--- /dev/null
+++ b/changelogs/unreleased/26852-fix-slug-for-openshift.yml
@@ -0,0 +1,4 @@
+---
+title: Avoid repeated dashes in $CI_ENVIRONMENT_SLUG
+merge_request: 8638
+author:
diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
new file mode 100644
index 00000000000..8dfabf87c2a
--- /dev/null
+++ b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml
@@ -0,0 +1,4 @@
+---
+title: Remove hover animation from row elements
+merge_request:
+author:
diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
new file mode 100644
index 00000000000..0e8f7093b34
--- /dev/null
+++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys
@@ -0,0 +1,4 @@
+---
+title: Refactor Timelogs structure to use foreign keys.
+merge_request: 8769
+author:
diff --git a/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
new file mode 100644
index 00000000000..ea567437ac2
--- /dev/null
+++ b/changelogs/unreleased/26920-hover-cursor-on-pagination-element.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes hover cursor on pipeline pagenation
+merge_request: 9003
+author:
diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
new file mode 100644
index 00000000000..c5c57af5aaf
--- /dev/null
+++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml
@@ -0,0 +1,4 @@
+---
+title: Improve pipeline status icon linking in widgets
+merge_request:
+author:
diff --git a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml b/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml
deleted file mode 100644
index 0f0a8940f72..00000000000
--- a/changelogs/unreleased/27044-fix-explore-sorting-on-trending.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix /explore sorting
-merge_request:
-author:
diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
new file mode 100644
index 00000000000..3f902fb324e
--- /dev/null
+++ b/changelogs/unreleased/27240-make-progress-bars-consistent.yml
@@ -0,0 +1,4 @@
+---
+title: 27240 Make progress bars consistent
+merge_request:
+author:
diff --git a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml b/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml
deleted file mode 100644
index 6e036923158..00000000000
--- a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix filtering with multiple words
-merge_request: 8830
-author:
diff --git a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml b/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml
deleted file mode 100644
index 2591f161bc5..00000000000
--- a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project name label's for reference in project settings
-merge_request: 8795
-author:
diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml
new file mode 100644
index 00000000000..7b307b501f4
--- /dev/null
+++ b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index
+merge_request: 8956
+author:
diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
new file mode 100644
index 00000000000..293aab67d39
--- /dev/null
+++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Unify MR diff file button style
+merge_request: 8874
+author:
diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
new file mode 100644
index 00000000000..502927cd160
--- /dev/null
+++ b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Only render hr when user can't archive project.
+merge_request: !8917
+author:
diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
new file mode 100644
index 00000000000..79316abbaf7
--- /dev/null
+++ b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml
@@ -0,0 +1,4 @@
+---
+title: Fix pipeline graph vertical spacing in Firefox and Safari
+merge_request: 8886
+author:
diff --git a/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml
new file mode 100644
index 00000000000..dd4907166c4
--- /dev/null
+++ b/changelogs/unreleased/27336-add-environment-url-link-to-terminal-page.yml
@@ -0,0 +1,4 @@
+---
+title: Added external environment link to web terminal view
+merge_request: 8303
+author:
diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml
new file mode 100644
index 00000000000..8f061a34ac0
--- /dev/null
+++ b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix filtered search user autocomplete for gitlab instances that are hosted
+ on a subdirectory
+merge_request: 8891
+author:
diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml
new file mode 100644
index 00000000000..191b530aee8
--- /dev/null
+++ b/changelogs/unreleased/27352-search-label-filter-header.yml
@@ -0,0 +1,4 @@
+---
+title: 27352-search-label-filter-header
+merge_request:
+author:
diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml
new file mode 100644
index 00000000000..dc400d65006
--- /dev/null
+++ b/changelogs/unreleased/27484-environment-show-name.yml
@@ -0,0 +1,4 @@
+---
+title: Don't capitalize environment name in show page
+merge_request:
+author:
diff --git a/changelogs/unreleased/27488-fix-jwt-version.yml b/changelogs/unreleased/27488-fix-jwt-version.yml
new file mode 100644
index 00000000000..5135ff0fd60
--- /dev/null
+++ b/changelogs/unreleased/27488-fix-jwt-version.yml
@@ -0,0 +1,4 @@
+---
+title: Update and pin the `jwt` gem to ~> 1.5.6
+merge_request:
+author:
diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml
new file mode 100644
index 00000000000..798c01f3238
--- /dev/null
+++ b/changelogs/unreleased/27494-environment-list-column-headers.yml
@@ -0,0 +1,4 @@
+---
+title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles
+merge_request:
+author:
diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
new file mode 100644
index 00000000000..bc990c66866
--- /dev/null
+++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong call to ProjectCacheWorker.perform
+merge_request: 8910
+author:
diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
new file mode 100644
index 00000000000..a5bb37ec8a9
--- /dev/null
+++ b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes flickering of avatar border in mention dropdown
+merge_request: 8950
+author:
diff --git a/changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml b/changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml
new file mode 100644
index 00000000000..8f297620e23
--- /dev/null
+++ b/changelogs/unreleased/27608-fixes-markdown-in-activity-feed-is-light-gray.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes markdown in activity-feed is gray
+merge_request: 9179
+author:
diff --git a/changelogs/unreleased/27610-issue-number-alignment.yml b/changelogs/unreleased/27610-issue-number-alignment.yml
new file mode 100644
index 00000000000..19ab8872c62
--- /dev/null
+++ b/changelogs/unreleased/27610-issue-number-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: fixes issue number alignment problem in MR and issue list
+merge_request: 9020
+author:
diff --git a/changelogs/unreleased/27632_fix_mr_widget_url.yml b/changelogs/unreleased/27632_fix_mr_widget_url.yml
new file mode 100644
index 00000000000..958621a43a1
--- /dev/null
+++ b/changelogs/unreleased/27632_fix_mr_widget_url.yml
@@ -0,0 +1,4 @@
+---
+title: Fix MR widget url
+merge_request: 8989
+author:
diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
new file mode 100644
index 00000000000..0531ef2c038
--- /dev/null
+++ b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml
@@ -0,0 +1,4 @@
+---
+title: Layer award emoji dropdown over the right sidebar
+merge_request: 9004
+author:
diff --git a/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml b/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml
new file mode 100644
index 00000000000..6c98b46d8cb
--- /dev/null
+++ b/changelogs/unreleased/27726-fix-dropdown-width-in-admin-project-page.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dropdown width in admin project page
+merge_request: 9002
+author:
diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
new file mode 100644
index 00000000000..aa89d9f9850
--- /dev/null
+++ b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml
@@ -0,0 +1,4 @@
+---
+title: Give ci status text on pipeline graph a better font-weight
+merge_request:
+author:
diff --git a/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml
new file mode 100644
index 00000000000..429110e9178
--- /dev/null
+++ b/changelogs/unreleased/27783-fix-fe-doc-broken-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes FE Doc broken link
+merge_request: 9120
+author:
diff --git a/changelogs/unreleased/27822-default-bulk-assign-labels.yml b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
new file mode 100644
index 00000000000..ee2431869f0
--- /dev/null
+++ b/changelogs/unreleased/27822-default-bulk-assign-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Add default labels to bulk assign dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
new file mode 100644
index 00000000000..4251754618b
--- /dev/null
+++ b/changelogs/unreleased/27880-pipelines-table-not-showing-commit-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes Pipelines table is not showing branch name for commit
+merge_request:
+author:
diff --git a/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
new file mode 100644
index 00000000000..fcbd48b0357
--- /dev/null
+++ b/changelogs/unreleased/27943-contribution-list-on-profile-page-is-aligned-right.yml
@@ -0,0 +1,4 @@
+---
+title: Fix contribution activity alignment
+merge_request:
+author:
diff --git a/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
new file mode 100644
index 00000000000..1dfabd3813b
--- /dev/null
+++ b/changelogs/unreleased/27947-missing-margin-between-loading-icon-and-text-in-merge-request-widget.yml
@@ -0,0 +1,4 @@
+---
+title: Add space between text and loading icon in Megre Request Widget
+merge_request: 9119
+author:
diff --git a/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
new file mode 100644
index 00000000000..d9f78db4bec
--- /dev/null
+++ b/changelogs/unreleased/27955-mr-notification-use-pipeline-language.yml
@@ -0,0 +1,4 @@
+---
+title: Show Pipeline(not Job) in MR desktop notification
+merge_request:
+author:
diff --git a/changelogs/unreleased/27963-tooltips-jobs.yml b/changelogs/unreleased/27963-tooltips-jobs.yml
new file mode 100644
index 00000000000..ba418d86433
--- /dev/null
+++ b/changelogs/unreleased/27963-tooltips-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Fix tooltips in mini pipeline graph
+merge_request:
+author:
diff --git a/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
new file mode 100644
index 00000000000..e4287d6276c
--- /dev/null
+++ b/changelogs/unreleased/27987-skipped-pipeline-mr-graph.yml
@@ -0,0 +1,4 @@
+---
+title: Show pipeline graph in MR widget if there are any stages
+merge_request:
+author:
diff --git a/changelogs/unreleased/27991-success-with-warnings-caret.yml b/changelogs/unreleased/27991-success-with-warnings-caret.yml
new file mode 100644
index 00000000000..703d34a5ede
--- /dev/null
+++ b/changelogs/unreleased/27991-success-with-warnings-caret.yml
@@ -0,0 +1,4 @@
+---
+title: Fix icon colors in merge request widget mini graph
+merge_request:
+author:
diff --git a/changelogs/unreleased/27994-fix-mr-widget-jump.yml b/changelogs/unreleased/27994-fix-mr-widget-jump.yml
new file mode 100644
index 00000000000..77783e54a3a
--- /dev/null
+++ b/changelogs/unreleased/27994-fix-mr-widget-jump.yml
@@ -0,0 +1,4 @@
+---
+title: Fix MR widget jump
+merge_request: 9146
+author:
diff --git a/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
new file mode 100644
index 00000000000..be2a0afbc52
--- /dev/null
+++ b/changelogs/unreleased/28029-improve-blockquote-formatting-on-emails.yml
@@ -0,0 +1,4 @@
+---
+title: Improve blockquote formatting in notification emails
+merge_request:
+author:
diff --git a/changelogs/unreleased/28032-tooltips-file-name.yml b/changelogs/unreleased/28032-tooltips-file-name.yml
new file mode 100644
index 00000000000..9fe11e7c2b6
--- /dev/null
+++ b/changelogs/unreleased/28032-tooltips-file-name.yml
@@ -0,0 +1,5 @@
+---
+title: Adds container to tooltip in order to make it work with overflow:hidden in
+ parent element
+merge_request:
+author:
diff --git a/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
new file mode 100644
index 00000000000..11d1f55172b
--- /dev/null
+++ b/changelogs/unreleased/395-fix-notification-when-group-set-to-watch.yml
@@ -0,0 +1,4 @@
+---
+title: Fix notifications when set at group level
+merge_request: 6813
+author: Alexandre Maia
diff --git a/changelogs/unreleased/8082-permalink-to-file.yml b/changelogs/unreleased/8082-permalink-to-file.yml
new file mode 100644
index 00000000000..136d2108c63
--- /dev/null
+++ b/changelogs/unreleased/8082-permalink-to-file.yml
@@ -0,0 +1,4 @@
+---
+title: Add `y` keyboard shortcut to move to file permalink
+merge_request:
+author:
diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml
new file mode 100644
index 00000000000..2f0f1887257
--- /dev/null
+++ b/changelogs/unreleased/9-0-api-changes.yml
@@ -0,0 +1,4 @@
+---
+title: Remove deprecated MR and Issue endpoints and preserve V3 namespace
+merge_request: 8967
+author:
diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
new file mode 100644
index 00000000000..9fd6ea5bc52
--- /dev/null
+++ b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml
@@ -0,0 +1,4 @@
+---
+title: Adds /target_branch slash command functionality for merge requests
+merge_request:
+author: YarNayar
diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml
new file mode 100644
index 00000000000..8a9e29109a8
--- /dev/null
+++ b/changelogs/unreleased/api-fix-files.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Fix file downloading'
+merge_request: Robert Schilling
+author: 8267
diff --git a/changelogs/unreleased/api-remove-snippets-expires-at.yml b/changelogs/unreleased/api-remove-snippets-expires-at.yml
new file mode 100644
index 00000000000..67603bfab3b
--- /dev/null
+++ b/changelogs/unreleased/api-remove-snippets-expires-at.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove deprecated ''expires_at'' from project snippets'
+merge_request: 8723
+author: Robert Schilling
diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml
new file mode 100644
index 00000000000..fda1c3bd562
--- /dev/null
+++ b/changelogs/unreleased/babel-all-the-things.yml
@@ -0,0 +1,5 @@
+---
+title: use babel to transpile all non-vendor javascript assets regardless of file
+ extension
+merge_request: 8988
+author:
diff --git a/changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml b/changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml
new file mode 100644
index 00000000000..f335ae27fda
--- /dev/null
+++ b/changelogs/unreleased/bypass-email-domain-validation-when-created-by-admin.yml
@@ -0,0 +1,4 @@
+---
+title: Bypass email domain validation when a user is created by an admin.
+merge_request: 8575
+author: Reza Mohammadi @remohammadi
diff --git a/changelogs/unreleased/cop-gem-fetcher.yml b/changelogs/unreleased/cop-gem-fetcher.yml
new file mode 100644
index 00000000000..506815a5b54
--- /dev/null
+++ b/changelogs/unreleased/cop-gem-fetcher.yml
@@ -0,0 +1,4 @@
+---
+title: Cop for gem fetched from a git source
+merge_request: 8856
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/copy-branch-to-clipboard.yml b/changelogs/unreleased/copy-branch-to-clipboard.yml
new file mode 100644
index 00000000000..c12e324ed3c
--- /dev/null
+++ b/changelogs/unreleased/copy-branch-to-clipboard.yml
@@ -0,0 +1,4 @@
+---
+title: Added the ability to copy a branch name to the clipboard
+merge_request: 9103
+author: Glenn Sayers
diff --git a/changelogs/unreleased/document-how-to-vue.yml b/changelogs/unreleased/document-how-to-vue.yml
new file mode 100644
index 00000000000..863e41b6413
--- /dev/null
+++ b/changelogs/unreleased/document-how-to-vue.yml
@@ -0,0 +1,4 @@
+---
+title: Adds documentation for how to use Vue.js
+merge_request: 8866
+author:
diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml
new file mode 100644
index 00000000000..fb589a053c0
--- /dev/null
+++ b/changelogs/unreleased/dont-delete-assigned-issuables.yml
@@ -0,0 +1,4 @@
+---
+title: Don't delete assigned MRs/issues when user is deleted
+merge_request:
+author:
diff --git a/changelogs/unreleased/dynamic-header-fixture.yml b/changelogs/unreleased/dynamic-header-fixture.yml
new file mode 100644
index 00000000000..9789a1999c8
--- /dev/null
+++ b/changelogs/unreleased/dynamic-header-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for header_spec.js
+merge_request: 9174
+author: winniehell
diff --git a/changelogs/unreleased/dz-create-nested-groups-via-ui.yml b/changelogs/unreleased/dz-create-nested-groups-via-ui.yml
new file mode 100644
index 00000000000..f9529a5941a
--- /dev/null
+++ b/changelogs/unreleased/dz-create-nested-groups-via-ui.yml
@@ -0,0 +1,4 @@
+---
+title: Allow creating nested groups via UI
+merge_request: 8786
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-api.yml b/changelogs/unreleased/dz-nested-groups-api.yml
new file mode 100644
index 00000000000..d33ff42700f
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add nested groups to the API
+merge_request: 9034
+author:
diff --git a/changelogs/unreleased/dz-nested-groups-improvements-2.yml b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
new file mode 100644
index 00000000000..8e4eb7f1fff
--- /dev/null
+++ b/changelogs/unreleased/dz-nested-groups-improvements-2.yml
@@ -0,0 +1,4 @@
+---
+title: Add read-only full_path and full_name attributes to Group API
+merge_request: 8827
+author:
diff --git a/changelogs/unreleased/dz-refactor-full-path.yml b/changelogs/unreleased/dz-refactor-full-path.yml
new file mode 100644
index 00000000000..da8568fd220
--- /dev/null
+++ b/changelogs/unreleased/dz-refactor-full-path.yml
@@ -0,0 +1,4 @@
+---
+title: Store group and project full name and full path in routes table
+merge_request: 8979
+author:
diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml
new file mode 100644
index 00000000000..5a42c98a800
--- /dev/null
+++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml
@@ -0,0 +1,4 @@
+---
+title: Change the reply shortcut to focus the field even without a selection.
+merge_request: 8873
+author: Brian Hall
diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml
new file mode 100644
index 00000000000..b5cc6bbf8b6
--- /dev/null
+++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: Use vue.js Pipelines table in commit and merge request view
+merge_request: 8844
+author:
diff --git a/changelogs/unreleased/fix-26518.yml b/changelogs/unreleased/fix-26518.yml
deleted file mode 100644
index 961ac2642fb..00000000000
--- a/changelogs/unreleased/fix-26518.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix access to the wiki code via HTTP when repository feature disabled
-merge_request: 8758
-author:
diff --git a/changelogs/unreleased/fix-27479.yml b/changelogs/unreleased/fix-27479.yml
new file mode 100644
index 00000000000..cc72a830695
--- /dev/null
+++ b/changelogs/unreleased/fix-27479.yml
@@ -0,0 +1,4 @@
+---
+title: Remove new branch button for confidential issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-anchor-scrolling.yml b/changelogs/unreleased/fix-anchor-scrolling.yml
new file mode 100644
index 00000000000..43b3b9bf96e
--- /dev/null
+++ b/changelogs/unreleased/fix-anchor-scrolling.yml
@@ -0,0 +1,4 @@
+---
+title: Fix broken anchor links when special characters are used
+merge_request: 8961
+author: Andrey Krivko
diff --git a/changelogs/unreleased/fix-cancel-integration-settings.yml b/changelogs/unreleased/fix-cancel-integration-settings.yml
deleted file mode 100644
index 294b0aa5db9..00000000000
--- a/changelogs/unreleased/fix-cancel-integration-settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed services form cancel not redirecting back the integrations settings view
-merge_request: 8843
-author:
diff --git a/changelogs/unreleased/fix-ci-build-policy.yml b/changelogs/unreleased/fix-ci-build-policy.yml
new file mode 100644
index 00000000000..26003713ed4
--- /dev/null
+++ b/changelogs/unreleased/fix-ci-build-policy.yml
@@ -0,0 +1,4 @@
+---
+title: Improve build policy and access abilities
+merge_request: 8711
+author:
diff --git a/changelogs/unreleased/fix-cycle-analytics-events-limit.yml b/changelogs/unreleased/fix-cycle-analytics-events-limit.yml
new file mode 100644
index 00000000000..152b37ca430
--- /dev/null
+++ b/changelogs/unreleased/fix-cycle-analytics-events-limit.yml
@@ -0,0 +1,4 @@
+---
+title: Add limit to the number of events showed in cycle analytics
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml
new file mode 100644
index 00000000000..e13215f22a7
--- /dev/null
+++ b/changelogs/unreleased/fix-deleting-project-again.yml
@@ -0,0 +1,4 @@
+---
+title: Fix deleting projects with pipelines and builds
+merge_request: 8960
+author:
diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml
new file mode 100644
index 00000000000..61817027720
--- /dev/null
+++ b/changelogs/unreleased/fix-depr-warn.yml
@@ -0,0 +1,4 @@
+---
+title: resolve deprecation warnings
+merge_request: 8855
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
new file mode 100644
index 00000000000..df7e3776700
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml
@@ -0,0 +1,4 @@
+---
+title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context
+merge_request: 8981
+author:
diff --git a/changelogs/unreleased/fix-import-encrypt-atts.yml b/changelogs/unreleased/fix-import-encrypt-atts.yml
new file mode 100644
index 00000000000..e34d895570b
--- /dev/null
+++ b/changelogs/unreleased/fix-import-encrypt-atts.yml
@@ -0,0 +1,4 @@
+---
+title: Ignore encrypted attributes in Import/Export
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-group-members.yml b/changelogs/unreleased/fix-import-group-members.yml
new file mode 100644
index 00000000000..fe580af31b3
--- /dev/null
+++ b/changelogs/unreleased/fix-import-group-members.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to export project inherited group members to Import/Export
+merge_request: 8923
+author:
diff --git a/changelogs/unreleased/fix-job-to-pipeline-renaming.yml b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
new file mode 100644
index 00000000000..d5f34b4b25d
--- /dev/null
+++ b/changelogs/unreleased/fix-job-to-pipeline-renaming.yml
@@ -0,0 +1,4 @@
+---
+title: Fix job to pipeline renaming
+merge_request: 9147
+author:
diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml
new file mode 100644
index 00000000000..b927279cdf4
--- /dev/null
+++ b/changelogs/unreleased/fix-references-header-parsing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix reply by email without sub-addressing for some clients from
+ Microsoft and Apple
+merge_request: 8620
+author:
diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml
new file mode 100644
index 00000000000..e98ac755b88
--- /dev/null
+++ b/changelogs/unreleased/fix-scroll-test.yml
@@ -0,0 +1,4 @@
+---
+title: Change rspec test to guarantee window is resized before visiting page
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-search-bar-search-param.yml b/changelogs/unreleased/fix-search-bar-search-param.yml
deleted file mode 100644
index 4df14d3bf13..00000000000
--- a/changelogs/unreleased/fix-search-bar-search-param.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix search bar search param encoding
-merge_request: 8753
-author:
diff --git a/changelogs/unreleased/fix_issue_from_milestone.yml b/changelogs/unreleased/fix_issue_from_milestone.yml
new file mode 100644
index 00000000000..02581e3ea09
--- /dev/null
+++ b/changelogs/unreleased/fix_issue_from_milestone.yml
@@ -0,0 +1,4 @@
+---
+title: fix milestone does not automatically assign when create issue from milestone
+merge_request:
+author:
diff --git a/changelogs/unreleased/fixes-namespace-api-documentation.yml b/changelogs/unreleased/fixes-namespace-api-documentation.yml
new file mode 100644
index 00000000000..6b578bb1602
--- /dev/null
+++ b/changelogs/unreleased/fixes-namespace-api-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Update API docs for new namespace format
+merge_request: 9073
+author: Markus Koller
diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml
new file mode 100644
index 00000000000..1427e4e7624
--- /dev/null
+++ b/changelogs/unreleased/fwn-to-find-by-full-path.yml
@@ -0,0 +1,4 @@
+---
+title: replace `find_with_namespace` with `find_by_full_path`
+merge_request: 8949
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml
new file mode 100644
index 00000000000..b2959c02c07
--- /dev/null
+++ b/changelogs/unreleased/git_to_html_redirection.yml
@@ -0,0 +1,4 @@
+---
+title: Redirect http://someproject.git to http://someproject
+merge_request:
+author: blackst0ne
diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml
new file mode 100644
index 00000000000..7f372ccb428
--- /dev/null
+++ b/changelogs/unreleased/go-go-gadget-webpack.yml
@@ -0,0 +1,4 @@
+---
+title: use webpack to bundle frontend assets and use karma for frontend testing
+merge_request: 7288
+author:
diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml
new file mode 100644
index 00000000000..c11c2d4ede1
--- /dev/null
+++ b/changelogs/unreleased/group-label-sidebar-link.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed group label links in issue/merge request sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/hardcode-title-system-note.yml b/changelogs/unreleased/hardcode-title-system-note.yml
new file mode 100644
index 00000000000..1b0a63efa51
--- /dev/null
+++ b/changelogs/unreleased/hardcode-title-system-note.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure autogenerated title does not cause failing spec
+merge_request: 8963
+author: brian m. carlson
diff --git a/changelogs/unreleased/improve-ci-example-php-doc.yml b/changelogs/unreleased/improve-ci-example-php-doc.yml
new file mode 100644
index 00000000000..39a85e3d261
--- /dev/null
+++ b/changelogs/unreleased/improve-ci-example-php-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Changed composer installer script in the CI PHP example doc
+merge_request: 4342
+author: Jeffrey Cafferata
diff --git a/changelogs/unreleased/improve-handleLocationHash-tests.yml b/changelogs/unreleased/improve-handleLocationHash-tests.yml
new file mode 100644
index 00000000000..8ae3dfe079c
--- /dev/null
+++ b/changelogs/unreleased/improve-handleLocationHash-tests.yml
@@ -0,0 +1,4 @@
+---
+title: Improve gl.utils.handleLocationHash tests
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml
new file mode 100644
index 00000000000..60da1c14702
--- /dev/null
+++ b/changelogs/unreleased/issue-20428.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to define a coverage regex in the .gitlab-ci.yml
+merge_request: 7447
+author: Leandro Camargo
diff --git a/changelogs/unreleased/issue-newproj-layout.yml b/changelogs/unreleased/issue-newproj-layout.yml
new file mode 100644
index 00000000000..d15e8b7d1e5
--- /dev/null
+++ b/changelogs/unreleased/issue-newproj-layout.yml
@@ -0,0 +1,4 @@
+---
+title: Removed duplicate "Visibility Level" label on New Project page
+merge_request:
+author: Robert Marcano
diff --git a/changelogs/unreleased/issue-sidebar-empty-assignee.yml b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
new file mode 100644
index 00000000000..263af75b9e9
--- /dev/null
+++ b/changelogs/unreleased/issue-sidebar-empty-assignee.yml
@@ -0,0 +1,4 @@
+---
+title: Resets assignee dropdown when sidebar is open
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_19262.yml b/changelogs/unreleased/issue_19262.yml
new file mode 100644
index 00000000000..5dea1493f23
--- /dev/null
+++ b/changelogs/unreleased/issue_19262.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow system notes for closed issuables
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_25900.yml b/changelogs/unreleased/issue_25900.yml
new file mode 100644
index 00000000000..b4b72b8a20c
--- /dev/null
+++ b/changelogs/unreleased/issue_25900.yml
@@ -0,0 +1,4 @@
+---
+title: Gather issuable metadata to avoid n+1 queries on index view
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_26701.yml b/changelogs/unreleased/issue_26701.yml
new file mode 100644
index 00000000000..6834351bf43
--- /dev/null
+++ b/changelogs/unreleased/issue_26701.yml
@@ -0,0 +1,4 @@
+---
+title: Remove JIRA closed status icon
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml
new file mode 100644
index 00000000000..ee4a43a93db
--- /dev/null
+++ b/changelogs/unreleased/jej-pages-picked-from-ee.yml
@@ -0,0 +1,4 @@
+---
+title: Added GitLab Pages to CE
+merge_request: 8463
+author:
diff --git a/changelogs/unreleased/label-promotion.yml b/changelogs/unreleased/label-promotion.yml
new file mode 100644
index 00000000000..2ab997bf420
--- /dev/null
+++ b/changelogs/unreleased/label-promotion.yml
@@ -0,0 +1,4 @@
+---
+title: "Project labels can now be promoted to group labels"
+merge_request: 7242
+author: Olaf Tomalka
diff --git a/changelogs/unreleased/label-select-toggle.yml b/changelogs/unreleased/label-select-toggle.yml
deleted file mode 100644
index af5b4246521..00000000000
--- a/changelogs/unreleased/label-select-toggle.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed label dropdown toggle text not correctly updating
-merge_request:
-author:
diff --git a/changelogs/unreleased/lfs-noauth-public-repo.yml b/changelogs/unreleased/lfs-noauth-public-repo.yml
new file mode 100644
index 00000000000..60f62d7691b
--- /dev/null
+++ b/changelogs/unreleased/lfs-noauth-public-repo.yml
@@ -0,0 +1,4 @@
+---
+title: Support unauthenticated LFS object downloads for public projects
+merge_request: 8824
+author: Ben Boeckel
diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml
new file mode 100644
index 00000000000..c855f0cbcf7
--- /dev/null
+++ b/changelogs/unreleased/markdown-plantuml.yml
@@ -0,0 +1,4 @@
+---
+title: PlantUML support for Markdown
+merge_request: 8588
+author: Horacio Sanson
diff --git a/changelogs/unreleased/new-branch-fixture.yml b/changelogs/unreleased/new-branch-fixture.yml
new file mode 100644
index 00000000000..ce5ed816102
--- /dev/null
+++ b/changelogs/unreleased/new-branch-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for new_branch_spec.js
+merge_request: 9131
+author: winniehell
diff --git a/changelogs/unreleased/no-sidebar-on-action-btn-click.yml b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
new file mode 100644
index 00000000000..09e0b3a12d8
--- /dev/null
+++ b/changelogs/unreleased/no-sidebar-on-action-btn-click.yml
@@ -0,0 +1,4 @@
+---
+title: dismiss sidebar on repo buttons click
+merge_request: 8798
+author: Adam Pahlevi
diff --git a/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
new file mode 100644
index 00000000000..0751047c3c0
--- /dev/null
+++ b/changelogs/unreleased/pass in current_user in MergeRequest and MergeRequestsHelper.yml
@@ -0,0 +1,4 @@
+---
+title: pass in current_user in MergeRequest and MergeRequestsHelper
+merge_request: 8624
+author: Dongqing Hu
diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml
new file mode 100644
index 00000000000..c2fa1a7fad0
--- /dev/null
+++ b/changelogs/unreleased/pms-lowercase-system-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Make all system notes lowercase
+merge_request: 8807
+author:
diff --git a/changelogs/unreleased/protected-branch-dropdown-titles.yml b/changelogs/unreleased/protected-branch-dropdown-titles.yml
new file mode 100644
index 00000000000..df82cc00fc9
--- /dev/null
+++ b/changelogs/unreleased/protected-branch-dropdown-titles.yml
@@ -0,0 +1,4 @@
+---
+title: Added headers to protected branch access dropdowns
+merge_request:
+author:
diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
new file mode 100644
index 00000000000..547a7c6755c
--- /dev/null
+++ b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml
@@ -0,0 +1,4 @@
+---
+title: Redesign searchbar in admin project list
+merge_request: 8776
+author:
diff --git a/changelogs/unreleased/refresh-authorizations-fork-join.yml b/changelogs/unreleased/refresh-authorizations-fork-join.yml
deleted file mode 100644
index b1349b9447d..00000000000
--- a/changelogs/unreleased/refresh-authorizations-fork-join.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix race conditions for AuthorizedProjectsWorker
-merge_request:
-author:
diff --git a/changelogs/unreleased/refresh-permissions-when-moving-projects.yml b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
new file mode 100644
index 00000000000..a94bcdaa9a3
--- /dev/null
+++ b/changelogs/unreleased/refresh-permissions-when-moving-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Refresh authorizations when transferring projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/removal_of_unused_parameter.yml b/changelogs/unreleased/removal_of_unused_parameter.yml
new file mode 100644
index 00000000000..26bffafd9d9
--- /dev/null
+++ b/changelogs/unreleased/removal_of_unused_parameter.yml
@@ -0,0 +1,4 @@
+---
+title: 'removed unused parameter ''status_only: true'''
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-deploy-key-endpoint.yml b/changelogs/unreleased/remove-deploy-key-endpoint.yml
new file mode 100644
index 00000000000..3ff69adb4d3
--- /dev/null
+++ b/changelogs/unreleased/remove-deploy-key-endpoint.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Remove /projects/:id/keys/.. endpoints'
+merge_request: 8716
+author: Robert Schilling
diff --git a/changelogs/unreleased/remove-jquery-ui-datepicker.yml b/changelogs/unreleased/remove-jquery-ui-datepicker.yml
new file mode 100644
index 00000000000..cd00690d774
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-datepicker.yml
@@ -0,0 +1,4 @@
+---
+title: Replaced jQuery UI datepicker
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-jquery-ui-sortable.yml b/changelogs/unreleased/remove-jquery-ui-sortable.yml
new file mode 100644
index 00000000000..35f47822738
--- /dev/null
+++ b/changelogs/unreleased/remove-jquery-ui-sortable.yml
@@ -0,0 +1,4 @@
+---
+title: Replaced jQuery UI sortable
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
new file mode 100644
index 00000000000..f42aa6fae79
--- /dev/null
+++ b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml
@@ -0,0 +1,4 @@
+---
+title: Don't use backup Active Record connections for Sidekiq
+merge_request:
+author:
diff --git a/changelogs/unreleased/rename_delete_services.yml b/changelogs/unreleased/rename_delete_services.yml
new file mode 100644
index 00000000000..686a1ef3d55
--- /dev/null
+++ b/changelogs/unreleased/rename_delete_services.yml
@@ -0,0 +1,4 @@
+---
+title: Fix inconsistent naming for services that delete things
+merge_request: 5803
+author: dixpac
diff --git a/changelogs/unreleased/rename_files_delete_service.yml b/changelogs/unreleased/rename_files_delete_service.yml
new file mode 100644
index 00000000000..4de1c5b0d63
--- /dev/null
+++ b/changelogs/unreleased/rename_files_delete_service.yml
@@ -0,0 +1,4 @@
+---
+title: Rename Files::DeleteService to Files::DestroyService
+merge_request: 9110
+author: dixpac
diff --git a/changelogs/unreleased/revert-filter-assigned-to-me.yml b/changelogs/unreleased/revert-filter-assigned-to-me.yml
deleted file mode 100644
index 37f9d2f5fc4..00000000000
--- a/changelogs/unreleased/revert-filter-assigned-to-me.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Revert 3f17f29a
-merge_request: 8785
-author:
diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml
new file mode 100644
index 00000000000..9b6df0c54af
--- /dev/null
+++ b/changelogs/unreleased/route-map.yml
@@ -0,0 +1,4 @@
+---
+title: Add 'View on [env]' link to blobs and individual files in diffs
+merge_request: 8867
+author:
diff --git a/changelogs/unreleased/rs-warden-blocked-users.yml b/changelogs/unreleased/rs-warden-blocked-users.yml
new file mode 100644
index 00000000000..c0c23fb6f11
--- /dev/null
+++ b/changelogs/unreleased/rs-warden-blocked-users.yml
@@ -0,0 +1,4 @@
+---
+title: Don't perform Devise trackable updates on blocked User records
+merge_request: 8915
+author:
diff --git a/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
new file mode 100644
index 00000000000..bab76812a17
--- /dev/null
+++ b/changelogs/unreleased/sh-add-index-to-ci-trigger-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Add index to ci_trigger_requests for commit_id
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml b/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml
deleted file mode 100644
index ff2b38f21f2..00000000000
--- a/changelogs/unreleased/sh-fix-annotated-tags-pointing-to-blob.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Error 500 when repositories contain annotated tags pointing to blobs
-merge_request:
-author:
diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml
new file mode 100644
index 00000000000..e6ffb94bd08
--- /dev/null
+++ b/changelogs/unreleased/slash-commands-typo.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed "substract" typo on /help/user/project/slash_commands
+merge_request: 8976
+author: Jason Aquino
diff --git a/changelogs/unreleased/static-navbar.yml b/changelogs/unreleased/static-navbar.yml
new file mode 100644
index 00000000000..eaf478a48d0
--- /dev/null
+++ b/changelogs/unreleased/static-navbar.yml
@@ -0,0 +1,4 @@
+---
+title: Remove fixed positioning from top nav
+merge_request: !7547
+author:
diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml
new file mode 100644
index 00000000000..db1e66770d1
--- /dev/null
+++ b/changelogs/unreleased/terminal-max-session-time.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce maximum session time for terminal websocket connection
+merge_request: 8413
+author:
diff --git a/changelogs/unreleased/upgrade-babel-v6.yml b/changelogs/unreleased/upgrade-babel-v6.yml
new file mode 100644
index 00000000000..55f9b3e407c
--- /dev/null
+++ b/changelogs/unreleased/upgrade-babel-v6.yml
@@ -0,0 +1,4 @@
+---
+title: upgrade babel 5.8.x to babel 6.22.x
+merge_request: 9072
+author:
diff --git a/changelogs/unreleased/upgrade-webpack-v2-2.yml b/changelogs/unreleased/upgrade-webpack-v2-2.yml
new file mode 100644
index 00000000000..6a49859d68c
--- /dev/null
+++ b/changelogs/unreleased/upgrade-webpack-v2-2.yml
@@ -0,0 +1,4 @@
+---
+title: upgrade to webpack v2.2
+merge_request: 9078
+author:
diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml
new file mode 100644
index 00000000000..2494884f5c9
--- /dev/null
+++ b/changelogs/unreleased/zj-format-chat-messages.yml
@@ -0,0 +1,4 @@
+---
+title: Reformat messages ChatOps
+merge_request: 8528
+author:
diff --git a/config/application.rb b/config/application.rb
index f00e58a36ca..9088d3c432b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -80,6 +80,14 @@ module Gitlab
# like if you have constraints or database-specific column types
# config.active_record.schema_format = :sql
+ # Configure webpack
+ config.webpack.config_file = "config/webpack.config.js"
+ config.webpack.output_dir = "public/assets/webpack"
+ config.webpack.public_path = "assets/webpack"
+
+ # Webpack dev server configuration is handled in initializers/static_files.rb
+ config.webpack.dev_server.enabled = false
+
# Enable the asset pipeline
config.assets.enabled = true
config.assets.paths << Gemojione.images_path
@@ -88,31 +96,13 @@ module Gitlab
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
- config.assets.precompile << "lib/vue_resource.js"
config.assets.precompile << "katex.css"
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
- config.assets.precompile << "graphs/graphs_bundle.js"
- config.assets.precompile << "users/users_bundle.js"
- config.assets.precompile << "network/network_bundle.js"
- config.assets.precompile << "profile/profile_bundle.js"
- config.assets.precompile << "protected_branches/protected_branches_bundle.js"
- config.assets.precompile << "diff_notes/diff_notes_bundle.js"
- config.assets.precompile << "merge_request_widget/ci_bundle.js"
- config.assets.precompile << "issuable/issuable_bundle.js"
- config.assets.precompile << "boards/boards_bundle.js"
- config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
- config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
- 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 << "terminal/terminal_bundle.js"
- config.assets.precompile << "filtered_search/filtered_search_bundle.js"
- config.assets.precompile << "lib/utils/*.js"
- config.assets.precompile << "lib/*.js"
+ config.assets.precompile << "lib/ace.js"
+ config.assets.precompile << "lib/cropper.js"
+ config.assets.precompile << "lib/raphael.js"
config.assets.precompile << "u2f.js"
- config.assets.precompile << "vue_pipelines_index/index.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index c11296975b7..7336d7c842a 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -1,9 +1,9 @@
---
-# IGNORED GROUPS AND GEMS
- - :ignore_group
- development
- :who: Connor Shea
- :why: Development gems are not distributed with the final product and are therefore exempt.
+ :why: Development gems are not distributed with the final product and are therefore
+ exempt.
:versions: []
:when: 2016-04-17 21:27:01.054140000 Z
- - :ignore_group
@@ -18,8 +18,6 @@
:why: Bundler is MIT licensed but will sometimes fail in CI.
:versions: []
:when: 2016-05-02 06:42:08.045090000 Z
-
-# LICENSE WHITELIST
- - :whitelist
- MIT
- :who: Connor Shea
@@ -86,9 +84,6 @@
:why: https://opensource.org/licenses/BSD-2-Clause
:versions: []
:when: 2016-07-26 21:24:07.248480000 Z
-
-
-# LICENSE BLACKLIST
- - :blacklist
- GPLv2
- :who: Connor Shea
@@ -107,9 +102,6 @@
:why: The OSL license is a copyleft license
:versions: []
:when: 2016-10-28 11:02:15.540105000 Z
-
-
-# GEM LICENSES
- - :license
- raphael-rails
- MIT
@@ -201,3 +193,130 @@
:why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-05-02 05:56:50.696858000 Z
+- - :approve
+ - after
+ - :who: Matt Lee
+ :why: https://github.com/Raynos/after/blob/master/LICENCE
+ :versions: []
+ :when: 2017-01-14 20:00:32.473125000 Z
+- - :approve
+ - amdefine
+ - :who: Matt Lee
+ :why: MIT License
+ :versions: []
+ :when: 2017-01-14 20:08:31.810633000 Z
+- - :approve
+ - base64id
+ - :who: Matt Lee
+ :why: https://github.com/faeldt/base64id/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:33.174760000 Z
+- - :approve
+ - blob
+ - :who: Matt Lee
+ :why: https://github.com/webmodules/blob/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:34.564048000 Z
+- - :approve
+ - callsite
+ - :who: Matt Lee
+ :why: https://github.com/tj/callsite/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:35.976025000 Z
+- - :approve
+ - component-bind
+ - :who: Matt Lee
+ :why: https://github.com/component/bind/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:08:37.291219000 Z
+- - :approve
+ - component-inherit
+ - :who: Matt Lee
+ :why: https://github.com/component/inherit/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:41.804804000 Z
+- - :approve
+ - fsevents
+ - :who: Matt Lee
+ :why: https://github.com/strongloop/fsevents/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:50:20.037775000 Z
+- - :approve
+ - indexof
+ - :who: Matt Lee
+ :why: https://github.com/component/indexof/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:43.209900000 Z
+- - :approve
+ - is-integer
+ - :who: Matt Lee
+ :why: https://github.com/parshap/js-is-integer/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:44.540916000 Z
+- - :approve
+ - jsonify
+ - :who: Matt Lee
+ :why: Public Domain - no formal license on this one. probably okay as its been
+ the same for along time. would prefer to see CC0
+ :versions: []
+ :when: 2017-01-14 20:10:45.857261000 Z
+- - :approve
+ - object-component
+ - :who: Matt Lee
+ :why: https://github.com/component/object/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:47.190148000 Z
+- - :approve
+ - optimist
+ - :who: Matt Lee
+ :why: https://github.com/substack/node-optimist/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:48.563077000 Z
+- - :approve
+ - path-is-inside
+ - :who: Matt Lee
+ :why: https://github.com/domenic/path-is-inside/blob/master/LICENSE.txt
+ :versions: []
+ :when: 2017-01-14 20:10:49.910497000 Z
+- - :approve
+ - rc
+ - :who: Matt Lee
+ :why: https://github.com/dominictarr/rc/blob/master/LICENSE.MIT
+ :versions: []
+ :when: 2017-01-14 20:10:51.244695000 Z
+- - :approve
+ - ripemd160
+ - :who: Matt Lee
+ :why: https://github.com/crypto-browserify/ripemd160/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:52.560282000 Z
+- - :approve
+ - select2
+ - :who: Matt Lee
+ :why: https://github.com/select2/select2/blob/master/LICENSE.md
+ :versions: []
+ :when: 2017-01-14 20:10:53.909618000 Z
+- - :approve
+ - tweetnacl
+ - :who: Matt Lee
+ :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE
+ :versions: []
+ :when: 2017-01-14 20:10:57.812077000 Z
+- - :approve
+ - wordwrap
+ - :who: Mike Greiling
+ :why: https://github.com/substack/node-wordwrap/blob/0.0.3/LICENSE
+ :versions: []
+ :when: 2017-02-08 20:17:13.084968000 Z
+- - :approve
+ - spdx-expression-parse
+ - :who: Mike Greiling
+ :why: https://github.com/kemitchell/spdx-expression-parse.js/blob/v1.0.4/LICENSE
+ :versions: []
+ :when: 2017-02-08 22:33:01.806977000 Z
+- - :approve
+ - spdx-license-ids
+ - :who: Mike Greiling
+ :why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE
+ :versions: []
+ :when: 2017-02-08 22:35:00.225232000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 42e5f105d46..cc1af77a1de 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -153,6 +153,21 @@ production: &base
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ ## GitLab Pages
+ pages:
+ enabled: false
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ # The domain under which the pages are served:
+ # http://group.example.com/project
+ # or project path can be a group page: group.example.com
+ host: example.com
+ port: 80 # Set to 443 if you serve the pages with HTTPS
+ https: false # Set to true if you serve the pages with HTTPS
+ # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
+ # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+
## Mattermost
## For enabling Add to Mattermost button
mattermost:
@@ -505,6 +520,16 @@ production: &base
# Git timeout to read a commit, in seconds
timeout: 10
+ ## Webpack settings
+ # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running
+ # on a given port instead of serving directly from /assets/webpack. This is only indended for use
+ # in development.
+ webpack:
+ # dev_server:
+ # enabled: true
+ # host: localhost
+ # port: 3808
+
#
# 5. Extra customization
# ==========================
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4f33aad8693..ab59394cb0c 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -6,7 +6,7 @@ class Settings < Settingslogic
class << self
def gitlab_on_standard_port?
- gitlab.port.to_i == (gitlab.https ? 443 : 80)
+ on_standard_port?(gitlab)
end
def host_without_www(url)
@@ -14,7 +14,7 @@ class Settings < Settingslogic
end
def build_gitlab_ci_url
- if gitlab_on_standard_port?
+ if on_standard_port?(gitlab)
custom_port = nil
else
custom_port = ":#{gitlab.port}"
@@ -27,6 +27,10 @@ class Settings < Settingslogic
].join('')
end
+ def build_pages_url
+ base_url(pages).join('')
+ end
+
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
@@ -42,11 +46,11 @@ class Settings < Settingslogic
end
def build_base_gitlab_url
- base_gitlab_url.join('')
+ base_url(gitlab).join('')
end
def build_gitlab_url
- (base_gitlab_url + [gitlab.relative_url_root]).join('')
+ (base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
@@ -74,15 +78,19 @@ class Settings < Settingslogic
private
- def base_gitlab_url
- custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}"
- [ gitlab.protocol,
+ def base_url(config)
+ custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
+ [ config.protocol,
"://",
- gitlab.host,
+ config.host,
custom_port
]
end
+ def on_standard_port?(config)
+ config.port.to_i == (config.https ? 443 : 80)
+ end
+
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
@@ -255,6 +263,20 @@ Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.regi
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
#
+# Pages
+#
+Settings['pages'] ||= Settingslogic.new({})
+Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
+Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root)
+Settings.pages['https'] = false if Settings.pages['https'].nil?
+Settings.pages['host'] ||= "example.com"
+Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
+Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
+Settings.pages['url'] ||= Settings.send(:build_pages_url)
+Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
+Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
+
+#
# Git LFS
#
Settings['lfs'] ||= Settingslogic.new({})
@@ -410,6 +432,15 @@ Settings['gitaly'] ||= Settingslogic.new({})
Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
#
+# Webpack settings
+#
+Settings['webpack'] ||= Settingslogic.new({})
+Settings.webpack['dev_server'] ||= Settingslogic.new({})
+Settings.webpack.dev_server['enabled'] ||= false
+Settings.webpack.dev_server['host'] ||= 'localhost'
+Settings.webpack.dev_server['port'] ||= 3808
+
+#
# Testing settings
#
if Rails.env.test?
diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb
new file mode 100644
index 00000000000..e8a77b146fa
--- /dev/null
+++ b/config/initializers/plantuml_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Plantuml
diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb
index a9aa802681a..fb5a7b8372e 100644
--- a/config/initializers/request_profiler.rb
+++ b/config/initializers/request_profiler.rb
@@ -1,5 +1,3 @@
-require 'gitlab/request_profiler/middleware'
-
Rails.application.configure do |config|
config.middleware.use(Gitlab::RequestProfiler::Middleware)
end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index f462e654b2c..0ef9f51e5cf 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -4,6 +4,12 @@ module RspecProfilingConnection
end
end
+module RspecProfilingGitBranchCi
+ def branch
+ ENV['CI_BUILD_REF_NAME'] || super
+ end
+end
+
if Rails.env.test?
RspecProfiling.configure do |config|
if ENV['RSPEC_PROFILING_POSTGRES_URL']
@@ -11,4 +17,6 @@ if Rails.env.test?
config.collector = RspecProfiling::Collectors::PSQL
end
end
+
+ RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI')
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index fa318384405..0c4516b70f0 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -36,11 +36,9 @@ Sidekiq.configure_server do |config|
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] ||
Rails.application.config.database_configuration[Rails.env]
- config['pool'] = Sidekiq.options[:concurrency] + 2
+ config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index d6dbf8b9fbf..74aba6c5d06 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -12,4 +12,35 @@ if app.config.serve_static_files
app.paths["public"].first,
app.config.static_cache_control
)
+
+ # If webpack-dev-server is configured, proxy webpack's public directory
+ # instead of looking for static assets
+ dev_server = Gitlab.config.webpack.dev_server
+
+ if dev_server.enabled
+ settings = {
+ enabled: true,
+ host: dev_server.host,
+ port: dev_server.port,
+ manifest_host: dev_server.host,
+ manifest_port: dev_server.port,
+ }
+
+ if Rails.env.development?
+ settings.merge!(
+ host: Gitlab.config.gitlab.host,
+ port: Gitlab.config.gitlab.port,
+ https: Gitlab.config.gitlab.https,
+ )
+ app.config.middleware.insert_before(
+ Gitlab::Middleware::Static,
+ Gitlab::Middleware::WebpackProxy,
+ proxy_path: app.config.webpack.public_path,
+ proxy_host: dev_server.host,
+ proxy_port: dev_server.port,
+ )
+ end
+
+ app.config.webpack.dev_server.merge!(settings)
+ end
end
diff --git a/config/karma.config.js b/config/karma.config.js
new file mode 100644
index 00000000000..44229e2ee88
--- /dev/null
+++ b/config/karma.config.js
@@ -0,0 +1,21 @@
+var path = require('path');
+var webpackConfig = require('./webpack.config.js');
+var ROOT_PATH = path.resolve(__dirname, '..');
+
+// Karma configuration
+module.exports = function(config) {
+ config.set({
+ basePath: ROOT_PATH,
+ browsers: ['PhantomJS'],
+ frameworks: ['jasmine'],
+ files: [
+ { pattern: 'spec/javascripts/test_bundle.js', watched: false },
+ { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
+ ],
+ preprocessors: {
+ 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+ },
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ });
+};
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 60a1175fe80..73f69d76995 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -25,5 +25,6 @@ scope(path: 'groups/*id',
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
+ get :subgroups, as: :subgroups_group
get '/', action: :show, as: :group_canonical
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 6620b765e02..2ac98cf3842 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -39,6 +39,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
+ resource :pages, only: [:show, :destroy] do
+ resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ }
+ end
+
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
@@ -64,6 +68,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
+ post :mark_as_spam
end
end
@@ -220,6 +225,7 @@ constraints(ProjectUrlConstrainer.new) do
end
member do
+ post :promote
post :toggle_subscription
delete :remove_priority
end
@@ -265,7 +271,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :boards, only: [:index, :show] do
scope module: :boards do
- resources :issues, only: [:update]
+ resources :issues, only: [:index, :update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
@@ -309,6 +315,7 @@ constraints(ProjectUrlConstrainer.new) do
end
namespace :settings do
resource :members, only: [:show]
+ resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 3ca096f31ba..ce0d1314292 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do
member do
get 'raw'
get 'download'
+ post :mark_as_spam
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 022b0e80917..56bf4e6b1de 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -50,3 +50,4 @@
- [reactive_caching, 1]
- [cronjob, 1]
- [default, 1]
+ - [pages, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
new file mode 100644
index 00000000000..00f448c1fbb
--- /dev/null
+++ b/config/webpack.config.js
@@ -0,0 +1,125 @@
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var webpack = require('webpack');
+var StatsPlugin = require('stats-webpack-plugin');
+var CompressionPlugin = require('compression-webpack-plugin');
+
+var ROOT_PATH = path.resolve(__dirname, '..');
+var IS_PRODUCTION = process.env.NODE_ENV === 'production';
+var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
+var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
+
+var config = {
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+ entry: {
+ application: './application.js',
+ blob_edit: './blob_edit/blob_edit_bundle.js',
+ boards: './boards/boards_bundle.js',
+ simulate_drag: './test_utils/simulate_drag.js',
+ cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
+ commit_pipelines: './commit/pipelines/pipelines_bundle.js',
+ diff_notes: './diff_notes/diff_notes_bundle.js',
+ environments: './environments/environments_bundle.js',
+ filtered_search: './filtered_search/filtered_search_bundle.js',
+ graphs: './graphs/graphs_bundle.js',
+ issuable: './issuable/issuable_bundle.js',
+ merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
+ merge_request_widget: './merge_request_widget/ci_bundle.js',
+ network: './network/network_bundle.js',
+ profile: './profile/profile_bundle.js',
+ protected_branches: './protected_branches/protected_branches_bundle.js',
+ snippet: './snippet/snippet_bundle.js',
+ terminal: './terminal/terminal_bundle.js',
+ users: './users/users_bundle.js',
+ lib_chart: './lib/chart.js',
+ lib_d3: './lib/d3.js',
+ lib_vue: './lib/vue_resource.js',
+ vue_pipelines: './vue_pipelines_index/index.js',
+ },
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ publicPath: '/assets/webpack/',
+ filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js'
+ },
+
+ devtool: 'inline-source-map',
+
+ module: {
+ rules: [
+ {
+ test: /\.(js|es6)$/,
+ exclude: /(node_modules|vendor\/assets)/,
+ loader: 'babel-loader',
+ options: {
+ presets: [
+ ["es2015", {"modules": false}],
+ 'stage-2'
+ ]
+ }
+ },
+ {
+ test: /\.(js|es6)$/,
+ exclude: /node_modules/,
+ loader: 'imports-loader',
+ options: 'this=>window'
+ }
+ ]
+ },
+
+ plugins: [
+ // manifest filename must match config.webpack.manifest_filename
+ // webpack-rails only needs assetsByChunkName to function properly
+ new StatsPlugin('manifest.json', {
+ chunkModules: false,
+ source: false,
+ chunks: false,
+ modules: false,
+ assets: true
+ }),
+ new CompressionPlugin({
+ asset: '[path].gz[query]',
+ }),
+ ],
+
+ resolve: {
+ extensions: ['.js', '.es6', '.js.es6'],
+ alias: {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
+ 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
+ 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ 'vue$': 'vue/dist/vue.js',
+ 'vue-resource$': 'vue-resource/dist/vue-resource.js'
+ }
+ }
+}
+
+if (IS_PRODUCTION) {
+ config.devtool = 'source-map';
+ config.plugins.push(
+ new webpack.NoErrorsPlugin(),
+ new webpack.LoaderOptionsPlugin({
+ minimize: true,
+ debug: false
+ }),
+ new webpack.optimize.UglifyJsPlugin({
+ sourceMap: true
+ }),
+ new webpack.DefinePlugin({
+ 'process.env': { NODE_ENV: JSON.stringify('production') }
+ })
+ );
+}
+
+if (IS_DEV_SERVER) {
+ config.devServer = {
+ port: DEV_SERVER_PORT,
+ headers: { 'Access-Control-Allow-Origin': '*' }
+ };
+ config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
+}
+
+module.exports = config;
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index c04afe97277..c304e0706dc 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -26,7 +26,7 @@ Gitlab::Seeder.quiet do
end
end
- project = Project.find_with_namespace('gitlab-org/gitlab-test')
+ project = Project.find_by_full_path('gitlab-org/gitlab-test')
params = {
source_branch: 'feature',
diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
new file mode 100644
index 00000000000..f3a663f805b
--- /dev/null
+++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb
@@ -0,0 +1,14 @@
+class AddPagesSizeToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false
+ end
+
+ def down
+ remove_column(:application_settings, :max_pages_size)
+ end
+end
diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb
new file mode 100644
index 00000000000..0e8507c7e9a
--- /dev/null
+++ b/db/migrate/20160210105555_create_pages_domain.rb
@@ -0,0 +1,16 @@
+class CreatePagesDomain < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :pages_domains do |t|
+ t.integer :project_id
+ t.text :certificate
+ t.text :encrypted_key
+ t.string :encrypted_key_iv
+ t.string :encrypted_key_salt
+ t.string :domain
+ end
+
+ add_index :pages_domains, :domain, unique: true
+ end
+end
diff --git a/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
index 15ad8e8bcbb..ac50035eba4 100644
--- a/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
+++ b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
@@ -1,9 +1,15 @@
class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false
end
+
+ def down
+ remove_column :protected_branches, :developers_can_merge
+ end
end
diff --git a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
index 296f1dfac7b..20a77000ba8 100644
--- a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
@@ -14,7 +14,11 @@ class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
end
+
+ def down
+ remove_column :spam_logs, :submitted_as_ham
+ end
end
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index 05e21af0584..d10f3a6d104 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -7,7 +7,7 @@ class AddGroupIdToLabels < ActiveRecord::Migration
def change
add_column :labels, :group_id, :integer
- add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade
+ add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
add_concurrent_index :labels, :group_id
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
index f49df6802a7..2abfe47b776 100644
--- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -28,6 +28,6 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
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
+ add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
end
end
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
index 97534679b59..d5c343dc527 100644
--- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -5,7 +5,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
def up
add_column :subscriptions, :project_id, :integer
- add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade
+ add_foreign_key :subscriptions, :projects, column: :project_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
end
def down
diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
new file mode 100644
index 00000000000..88aa5d52b39
--- /dev/null
+++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddCoverageRegexToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :coverage_regex, :string
+ end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
index 7153e6a32b1..8e98ee5b9ba 100644
--- a/db/migrate/20161207231626_add_environment_slug.rb
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -8,8 +8,9 @@ class AddEnvironmentSlug < ActiveRecord::Migration
DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
# Used to generate random suffixes for the slug
+ LETTERS = 'a'..'z'
NUMBERS = '0'..'9'
- SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+ SUFFIX_CHARS = LETTERS.to_a + NUMBERS.to_a
def up
environments = Arel::Table.new(:environments)
@@ -39,17 +40,24 @@ class AddEnvironmentSlug < ActiveRecord::Migration
slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
# Must start with a letter
- slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+ slugified = 'env-' + slugified unless LETTERS.cover?(slugified[0])
+
+ # Repeated dashes are invalid (OpenShift limitation)
+ slugified.gsub!(/\-+/, '-')
# Maximum length: 24 characters (OpenShift limitation)
slugified = slugified[0..23]
- # Cannot end with a "-" character (Kubernetes label limitation)
- slugified = slugified[0..-2] if slugified[-1] == "-"
+ # Cannot end with a dash (Kubernetes label limitation)
+ slugified.chop! if slugified.end_with?('-')
# Add a random suffix, shortening the current string if necessary, if it
# has been slugified. This ensures uniqueness.
- slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+ if slugified != name
+ slugified = slugified[0..16]
+ slugified << '-' unless slugified.end_with?('-')
+ slugified << random_suffix
+ end
slugified
end
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
new file mode 100644
index 00000000000..69bfa2d3fc4
--- /dev/null
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -0,0 +1,54 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddForeignKeysToTimelogs < 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 up
+ change_table :timelogs do |t|
+ t.column :issue_id, :integer
+ t.column :merge_request_id, :integer
+ end
+
+ add_concurrent_index :timelogs, :issue_id
+ add_concurrent_index :timelogs, :merge_request_id
+
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
+ ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
+ EOF
+ else
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
+ execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
+ end
+
+ Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
+ Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
+ end
+
+ def down
+ Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
+ Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
+
+ remove_columns :timelogs, :issue_id, :merge_request_id
+ end
+end
diff --git a/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb
new file mode 100644
index 00000000000..334f53f9145
--- /dev/null
+++ b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.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 AddTerminalMaxSessionTimeToApplicationSettings < 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 up
+ add_column_with_default :application_settings, :terminal_max_session_time, :integer, default: 0, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :terminal_max_session_time
+ end
+end
diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
new file mode 100644
index 00000000000..0ee4229d1f8
--- /dev/null
+++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb
@@ -0,0 +1,17 @@
+class RemoveBacklogListsFromBoards < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute <<-SQL
+ DELETE FROM lists WHERE list_type = 0;
+ SQL
+ end
+
+ def down
+ execute <<-SQL
+ INSERT INTO lists (board_id, list_type, created_at, updated_at)
+ SELECT boards.id, 0, NOW(), NOW()
+ FROM boards;
+ SQL
+ end
+end
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
new file mode 100644
index 00000000000..e9a0aee4d6a
--- /dev/null
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -0,0 +1,11 @@
+class AddIndexToProjectAuthorizations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:project_authorizations, :project_id)
+ end
+end
diff --git a/db/migrate/20170204172458_add_name_to_route.rb b/db/migrate/20170204172458_add_name_to_route.rb
new file mode 100644
index 00000000000..38ed1ad9039
--- /dev/null
+++ b/db/migrate/20170204172458_add_name_to_route.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNameToRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :routes, :name, :string
+ end
+end
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
new file mode 100644
index 00000000000..8f944930807
--- /dev/null
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -0,0 +1,11 @@
+class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :labels, [:type, :project_id]
+ end
+end
diff --git a/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
new file mode 100644
index 00000000000..44372334d21
--- /dev/null
+++ b/db/migrate/20170206071414_add_recaptcha_verified_to_spam_logs.rb
@@ -0,0 +1,15 @@
+class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:spam_logs, :recaptcha_verified)
+ end
+end
diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
new file mode 100644
index 00000000000..61e49c14fc0
--- /dev/null
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -0,0 +1,11 @@
+class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_trigger_requests, :commit_id
+ end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
new file mode 100644
index 00000000000..89aa753646c
--- /dev/null
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # 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
+ remove_columns :timelogs, :trackable_id, :trackable_type
+ end
+end
diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
new file mode 100644
index 00000000000..f397ef919cc
--- /dev/null
+++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb
@@ -0,0 +1,32 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # 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 up
+ if Gitlab::Database.postgresql?
+ execute <<-EOF
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
+ ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
+ EOF
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3c836db27fc..d71911eaf14 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: 20170121130655) do
+ActiveRecord::Schema.define(version: 20170210075922) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -98,17 +98,19 @@ ActiveRecord::Schema.define(version: 20170121130655) 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
t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false
+ t.boolean "sidekiq_throttling_enabled", default: false
+ t.string "sidekiq_throttling_queues"
+ t.decimal "sidekiq_throttling_factor"
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
+ t.integer "max_pages_size", default: 100, null: false
+ t.integer "terminal_max_session_time", default: 0, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -215,6 +217,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do
t.datetime "queued_at"
t.string "token"
t.integer "lock_version"
+ t.string "coverage_regex"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -364,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170121130655) do
t.integer "commit_id"
end
+ add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
+
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
t.integer "project_id"
@@ -575,6 +580,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
+ add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false
@@ -854,6 +860,17 @@ ActiveRecord::Schema.define(version: 20170121130655) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "pages_domains", force: :cascade do |t|
+ t.integer "project_id"
+ t.text "certificate"
+ t.text "encrypted_key"
+ t.string "encrypted_key_iv"
+ t.string "encrypted_key_salt"
+ t.string "domain"
+ end
+
+ add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree
+
create_table "personal_access_tokens", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
@@ -874,6 +891,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do
t.integer "access_level"
end
+ add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
create_table "project_features", force: :cascade do |t|
@@ -1021,6 +1039,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do
t.string "path", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "name"
end
add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
@@ -1098,6 +1117,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
+ t.boolean "recaptcha_verified", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -1134,14 +1154,15 @@ ActiveRecord::Schema.define(version: 20170121130655) do
create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false
- t.integer "trackable_id"
- t.string "trackable_type"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "issue_id"
+ t.integer "merge_request_id"
end
- add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
+ add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
+ add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
create_table "todos", force: :cascade do |t|
@@ -1323,6 +1344,8 @@ ActiveRecord::Schema.define(version: 20170121130655) do
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 "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
+ add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index 909740211a6..1943d656aa7 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -12,16 +12,17 @@
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages.
- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
-- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
+- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
-- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
+- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
- [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.
@@ -53,8 +54,9 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
+- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages.
- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
-- [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics.
+- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md) Configure GitLab and Prometheus for measuring performance metrics.
- [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests.
- [Monitoring uptime](user/admin_area/monitoring/health_check.md) Check the server status using the health check endpoint.
- [Debugging Tips](administration/troubleshooting/debug.md) Tips to debug problems when things go wrong
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d7cfb464f74..a6300e18dc0 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -379,6 +379,10 @@ Read more about the individual driver's config options in the
filesystem. Remember to enable backups with your object storage provider if
desired.
+> **Important** Enabling storage driver other than `filesystem` would mean
+that your Docker client needs to be able to access the storage backend directly.
+So you must use an address that resolves and is accessible outside GitLab server.
+
---
**Omnibus GitLab installations**
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 80e5d80aa41..4d35b20d0c3 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -3,7 +3,7 @@
>
**Note:** Custom Git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not
+Please explore [webhooks] as an option if you do not
have filesystem access. For a user configurable Git hook interface, please see
[GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html).
@@ -80,5 +80,6 @@ STDERR takes precedence over STDOUT.
![Custom message from custom Git hook](img/custom_hooks_error_msg.png)
[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[webhooks]: ../user/project/integrations/webhooks.md
[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 1824829903c..dad8e956c0e 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -66,4 +66,4 @@ Read more on high-availability configuration:
configure custom domains with custom SSL, which would not be possible
if SSL was terminated at the load balancer.
-[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html
+[gitlab-pages]: ../pages/index.md
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 5602d70f1ef..3893d837006 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -47,13 +47,13 @@ When using default Omnibus configuration you will need to share 5 data locations
between all GitLab cluster nodes. No other locations should be shared. The
following are the 5 locations you need to mount:
-| Location | Description |
-| -------- | ----------- |
-| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data
-| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services
-| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments
-| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data
-| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces
+| Location | Description | Default configuration |
+| -------- | ----------- | --------------------- |
+| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
+| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
+| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
+| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
+| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
Other GitLab directories should not be shared between nodes. They contain
node-specific files and GitLab code that does not need to be shared. To ship
@@ -73,10 +73,10 @@ as subdirectories. Mount `/gitlab-data` then use the following Omnibus
configuration to move each data location to a subdirectory:
```ruby
+git_data_dirs({"default" => "/gitlab-data/git-data"})
user['home'] = '/gitlab-data/home'
-git_data_dir '/gitlab-data/git-data'
-gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_rails['uploads_directory'] = '/gitlab-data/uploads'
+gitlab_rails['shared_path'] = '/gitlab-data/shared'
gitlab_ci['builds_directory'] = '/gitlab-data/builds'
```
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index e5cf592e0a6..6515b1a264a 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -3,8 +3,8 @@
> [Introduced][ce-7810] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in
-GitLab we are able to create simple diagrams in AsciiDoc documents created in
-snippets, wikis, and repos.
+GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
+created in snippets, wikis, and repos.
## PlantUML Server
@@ -54,7 +54,7 @@ that, login with an Admin account and do following:
## Creating Diagrams
With PlantUML integration enabled and configured, we can start adding diagrams to
-our AsciiDoc snippets, wikis and repos using blocks:
+our AsciiDoc snippets, wikis and repos using delimited blocks:
```
[plantuml, format="png", id="myDiagram", width="200px"]
@@ -64,7 +64,14 @@ Alice -> Bob : Go Away
--
```
-The above block will be converted to an HTML img tag with source pointing to the
+And in Markdown using fenced code blocks:
+
+ ```plantuml
+ Bob -> Alice : hello
+ Alice -> Bob : Go Away
+ ```
+
+The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
render a nice diagram instead of the block:
@@ -77,7 +84,7 @@ Inside the block you can add any of the supported diagrams by PlantUML such as
and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML
diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block.
-Some parameters can be added to the block definition:
+Some parameters can be added to the AsciiDoc block definition:
- *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
all browsers so use with care. The default is `png`.
@@ -85,3 +92,4 @@ Some parameters can be added to the block definition:
- *width*: Width attribute added to the img tag.
- *height*: Height attribute added to the img tag.
+Markdown does not support any parameters and will always use PNG format.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index a1d1bb03b50..3b5ee86b68b 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -1,13 +1,12 @@
# Web terminals
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
-in GitLab 8.15. Only project masters and owners can access web terminals.
+> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can
+ access web terminals.
-With the introduction of the [Kubernetes](../../project_services/kubernetes.md)
-project service, GitLab gained the ability to store and use credentials for a
-Kubernetes cluster. One of the things it uses these credentials for is providing
-access to [web terminals](../../ci/environments.html#web-terminals)
-for environments.
+With the introduction of the [Kubernetes project service][kubservice], GitLab
+gained the ability to store and use credentials for a Kubernetes cluster. One
+of the things it uses these credentials for is providing access to
+[web terminals](../../ci/environments.html#web-terminals) for environments.
## How it works
@@ -71,3 +70,16 @@ by the above guides.
When these headers are not passed through, Workhorse will return a
`400 Bad Request` response to users attempting to use a web terminal. In turn,
they will receive a `Connection failed` message.
+
+## Limiting WebSocket connection time
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8413)
+in GitLab 8.17.
+
+Terminal sessions use long-lived connections; by default, these may last
+forever. You can configure a maximum session time in the Admin area of your
+GitLab instance if you find this undesirable from a scalability or security
+point of view.
+
+[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
+[kubservice]: ../../user/project/integrations/kubernetes.md
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 8b106e89cc2..17c2b4b70d3 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -15,7 +15,7 @@ documents in order to understand and properly configure GitLab Performance Monit
>**Note:**
Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
metrics. It will eventually replace InfluxDB when their metrics collection is
-on par. Read more in the [Prometheus documentation](prometheus.md).
+on par. Read more in the [Prometheus documentation](../prometheus/index.md).
## Introduction to GitLab Performance Monitoring
diff --git a/doc/administration/monitoring/performance/prometheus.md b/doc/administration/monitoring/performance/prometheus.md
index 51c63325064..d73ef5d1789 100644
--- a/doc/administration/monitoring/performance/prometheus.md
+++ b/doc/administration/monitoring/performance/prometheus.md
@@ -1,102 +1 @@
-# GitLab Prometheus
-
->**Notes:**
-- Prometheus and the node exporter are bundled in the Omnibus GitLab package
- since GitLab 8.16. For installations from source you will have to install
- them yourself. Over subsequent releases additional GitLab metrics will be
- captured.
-- Prometheus services are off by default but will be on starting with GitLab 9.0.
-
-[Prometheus] is a powerful time-series monitoring service, providing a flexible
-platform for monitoring GitLab and other software products.
-GitLab provides out of the box monitoring with Prometheus, providing easy
-access to high quality time-series monitoring of GitLab services.
-
-## Overview
-
-Prometheus works by periodically connecting to data sources and collecting their
-performance metrics. To view and work with the monitoring data, you can either
-connect directly to Prometheus or utilize a dashboard tool like [Grafana].
-
-## Configuring Prometheus
-
->**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
-
-To enable Prometheus:
-
-1. Edit `/etc/gitlab/gitlab.rb`
-1. Find and uncomment the following line, making sure it's set to `true`:
-
- ```ruby
- prometheus['enable'] = true
- ```
-
-1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
- take effect
-
-By default, Prometheus will run as the `gitlab-prometheus` user and listen on
-TCP port `9090` under localhost. If the [node exporter](#node-exporter) service
-has been enabled, it will automatically be set up as a monitoring target for
-Prometheus.
-
-## Viewing Performance Metrics
-
-After you have [enabled Prometheus](#configuring-prometheus), you can visit
-`<your_domain_name>:9090` for the dashboard that Prometheus offers by default.
-
-The performance data collected by Prometheus can be viewed directly in the
-Prometheus console or through a compatible dashboard tool.
-The Prometheus interface provides a [flexible query language][prom-query] to work
-with the collected data where you can visualize their output.
-For a more fully featured dashboard, Grafana can be used and has
-[official support for Prometheus][prom-grafana].
-
-## Prometheus exporters
-
-There are a number of libraries and servers which help in exporting existing
-metrics from third-party systems as Prometheus metrics. This is useful for cases
-where it is not feasible to instrument a given system with Prometheus metrics
-directly (for example, HAProxy or Linux system stats). You can read more in the
-[Prometheus exporters and integrations documentation][prom-exporters].
-
-While you can use any exporter you like with your GitLab installation, the
-following ones documented here are bundled in the Omnibus GitLab packages
-making it easy to configure and use.
-
-### Node exporter
-
->**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
-
-The [node exporter] allows you to measure various machine resources such as
-memory, disk and CPU utilization.
-
-To enable the node exporter:
-
-1. [Enable Prometheus](#configuring-prometheus)
-1. Edit `/etc/gitlab/gitlab.rb`
-1. Find and uncomment the following line, making sure it's set to `true`:
-
- ```ruby
- node_exporter['enable'] = true
- ```
-
-1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
- take effect
-
-Prometheus it will now automatically begin collecting performance data from
-the node exporter. You can visit `<your_domain_name>:9100/metrics` for a real
-time representation of the metrics that are collected. Refresh the page and
-you will see the data change.
-
-[grafana]: https://grafana.net
-[node exporter]: https://github.com/prometheus/node_exporter
-[prometheus]: https://prometheus.io
-[prom-query]: https://prometheus.io/docs/querying/basics
-[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
-[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
-[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
-[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
+This document was moved to [monitoring/prometheus](../prometheus/index.md).
diff --git a/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
new file mode 100644
index 00000000000..86ef9d167e2
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/gitlab_monitor_exporter.md
@@ -0,0 +1,30 @@
+# GitLab monitor exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1132]. For installations from source
+you'll have to install and configure it yourself.
+
+The [GitLab monitor exporter] allows you to measure various GitLab metrics.
+
+To enable the GitLab monitor exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ gitlab_monitor_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the GitLab monitor exporter exposed under `localhost:9168`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1132]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1132
+[GitLab monitor exporter]: https://gitlab.com/gitlab-org/gitlab-monitor
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
new file mode 100644
index 00000000000..3a394c561db
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -0,0 +1,147 @@
+# GitLab Prometheus
+
+>**Notes:**
+- Prometheus and the various exporters listed in this page are bundled in the
+ Omnibus GitLab package. Check each exporter's documentation for the timeline
+ they got added. For installations from source you will have to install
+ them yourself. Over subsequent releases additional GitLab metrics will be
+ captured.
+- Prometheus services are off by default but will be on starting with GitLab 9.0.
+- Prometheus and its exporters do not authenticate users, and will be available
+ to anyone who can access them.
+
+[Prometheus] is a powerful time-series monitoring service, providing a flexible
+platform for monitoring GitLab and other software products.
+GitLab provides out of the box monitoring with Prometheus, providing easy
+access to high quality time-series monitoring of GitLab services.
+
+## Overview
+
+Prometheus works by periodically connecting to data sources and collecting their
+performance metrics via the [various exporters](#prometheus-exporters). To view
+and work with the monitoring data, you can either
+[connect directly to Prometheus](#viewing-performance-metrics) or utilize a
+dashboard tool like [Grafana].
+
+## Configuring Prometheus
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+To enable Prometheus:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ prometheus['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+By default, Prometheus will run as the `gitlab-prometheus` user and listen on
+`http://localhost:9090`. If the [node exporter](#node-exporter) service
+has been enabled, it will automatically be set up as a monitoring target for
+Prometheus.
+
+## Changing the port Prometheus listens on
+
+>**Note:**
+The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
+it's not recommended to change the default address and port Prometheus listens
+on as this might affect or conflict with other services running on the GitLab
+server. Proceed at your own risk.
+
+To change the address/port that Prometheus listens on:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line:
+
+ ```ruby
+ prometheus['listen_address'] = 'localhost:9090'
+ ```
+
+ Replace `localhost:9090` with the address/port you want Prometheus to
+ listen on.
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+## Viewing performance metrics
+
+After you have [enabled Prometheus](#configuring-prometheus), you can visit
+`http://localhost:9090` for the dashboard that Prometheus offers by default.
+
+>**Note:**
+If SSL has been enabled on your GitLab instance, you may not be able to access
+Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
+[provide access via GitLab][multi-user-prometheus], but in the interim there are
+some workarounds: using a separate browser for Prometheus, resetting HSTS, or
+having [Nginx proxy it][nginx-custom-config]. Follow issue [#27069] for more
+information.
+
+The performance data collected by Prometheus can be viewed directly in the
+Prometheus console or through a compatible dashboard tool.
+The Prometheus interface provides a [flexible query language][prom-query] to work
+with the collected data where you can visualize their output.
+For a more fully featured dashboard, Grafana can be used and has
+[official support for Prometheus][prom-grafana].
+
+Sample Prometheus queries:
+
+- **% Memory used:** `(1 - ((node_memory_MemFree + node_memory_Cached) / node_memory_MemTotal)) * 100`
+- **% CPU load:** `1 - rate(node_cpu{mode="idle"}[5m])`
+- **Data transmitted:** `irate(node_network_transmit_bytes[5m])`
+- **Data received:** `irate(node_network_receive_bytes[5m])`
+
+## Prometheus exporters
+
+There are a number of libraries and servers which help in exporting existing
+metrics from third-party systems as Prometheus metrics. This is useful for cases
+where it is not feasible to instrument a given system with Prometheus metrics
+directly (for example, HAProxy or Linux system stats). You can read more in the
+[Prometheus exporters and integrations upstream documentation][prom-exporters].
+
+While you can use any exporter you like with your GitLab installation, the
+following ones documented here are bundled in the Omnibus GitLab packages
+making it easy to configure and use.
+
+### Node exporter
+
+The node exporter allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+[➔ Read more about the node exporter.](node_exporter.md)
+
+### Redis exporter
+
+The Redis exporter allows you to measure various Redis metrics.
+
+[➔ Read more about the Redis exporter.](redis_exporter.md)
+
+### Postgres exporter
+
+The Postgres exporter allows you to measure various PostgreSQL metrics.
+
+[➔ Read more about the Postgres exporter.](postgres_exporter.md)
+
+### GitLab monitor exporter
+
+The GitLab monitor exporter allows you to measure various GitLab metrics.
+
+[➔ Read more about the GitLab monitor exporter.](gitlab_monitor_exporter.md)
+
+[grafana]: https://grafana.net
+[hsts]: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
+[multi-user-prometheus]: https://gitlab.com/gitlab-org/multi-user-prometheus
+[nginx-custom-config]: https://docs.gitlab.com/omnibus/settings/nginx.html#inserting-custom-nginx-settings-into-the-gitlab-server-block
+[prometheus]: https://prometheus.io
+[prom-exporters]: https://prometheus.io/docs/instrumenting/exporters/
+[prom-query]: https://prometheus.io/docs/querying/basics
+[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
+[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
+[#27069]: https://gitlab.com/gitlab-org/gitlab-ce/issues/27069
+[1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
diff --git a/doc/administration/monitoring/prometheus/node_exporter.md b/doc/administration/monitoring/prometheus/node_exporter.md
new file mode 100644
index 00000000000..aef7758a88f
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/node_exporter.md
@@ -0,0 +1,30 @@
+# Node exporter
+
+>**Note:**
+Available since Omnibus GitLab 8.16. For installations from source you'll
+have to install and configure it yourself.
+
+The [node exporter] allows you to measure various machine resources such as
+memory, disk and CPU utilization.
+
+To enable the node exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ node_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the node exporter exposed under `localhost:9100`.
+
+[← Back to the main Prometheus page](index.md)
+
+[node exporter]: https://github.com/prometheus/node_exporter
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/postgres_exporter.md b/doc/administration/monitoring/prometheus/postgres_exporter.md
new file mode 100644
index 00000000000..8e2d3162f88
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/postgres_exporter.md
@@ -0,0 +1,30 @@
+# Postgres exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1131]. For installations from source
+you'll have to install and configure it yourself.
+
+The [postgres exporter] allows you to measure various PostgreSQL metrics.
+
+To enable the postgres exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ postgres_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the postgres exporter exposed under `localhost:9187`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1131]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1131
+[postgres exporter]: https://github.com/wrouesnel/postgres_exporter
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/monitoring/prometheus/redis_exporter.md b/doc/administration/monitoring/prometheus/redis_exporter.md
new file mode 100644
index 00000000000..d54d409dbb6
--- /dev/null
+++ b/doc/administration/monitoring/prometheus/redis_exporter.md
@@ -0,0 +1,33 @@
+# Redis exporter
+
+>**Note:**
+Available since [Omnibus GitLab 8.17][1118]. For installations from source
+you'll have to install and configure it yourself.
+
+The [Redis exporter] allows you to measure various [Redis] metrics. For more
+information on what's exported [read the upstream documentation][redis-exp].
+
+To enable the Redis exporter:
+
+1. [Enable Prometheus](index.md#configuring-prometheus)
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line, making sure it's set to `true`:
+
+ ```ruby
+ redis_exporter['enable'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
+Prometheus will now automatically begin collecting performance data from
+the Redis exporter exposed under `localhost:9121`.
+
+[← Back to the main Prometheus page](index.md)
+
+[1118]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1118
+[redis]: https://redis.io
+[redis exporter]: https://github.com/oliver006/redis_exporter
+[redis-exp]: https://github.com/oliver006/redis_exporter/blob/master/README.md#whats-exported
+[prometheus]: https://prometheus.io
+[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
new file mode 100644
index 00000000000..c352caf1115
--- /dev/null
+++ b/doc/administration/pages/index.md
@@ -0,0 +1,249 @@
+# GitLab Pages Administration
+
+> **Notes:**
+- [Introduced][ee-80] in GitLab EE 8.3.
+- Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+- This guide is for Omnibus GitLab installations. If you have installed
+ GitLab from source, follow the [Pages source installation document](source.md).
+
+---
+
+This document describes how to set up the _latest_ GitLab Pages feature. Make
+sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
+version as it may include new features and changes needed to be made in your
+configuration.
+
+If you are looking for ways to upload your static content in GitLab Pages, you
+probably want to read the [user documentation][pages-userguide].
+
+## Overview
+
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
+
+---
+
+In the case of custom domains, the Pages daemon needs to listen on ports `80`
+and/or `443`. For that reason, there is some flexibility in the way which you
+can set it up:
+
+1. Run the pages daemon in the same server as GitLab, listening on a secondary IP
+1. Run the pages daemon in a separate server. In that case, the
+ [Pages path](#change-storage-path) must also be present in the server that
+ the pages daemon is installed, so you will have to share it via network.
+1. Run the pages daemon in the same server as GitLab, listening on the same IP
+ but on different ports. In that case, you will have to proxy the traffic with
+ a loadbalancer. If you choose that route note that you should use TCP load
+ balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+ pages will not be able to be served with user provided certificates. For
+ HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option.
+
+## Prerequisites
+
+Before proceeding with the Pages configuration, you will need to:
+
+1. Have a separate domain under which the GitLab Pages will be served. In this
+ document we assume that to be `example.io`.
+1. Configure a **wildcard DNS record**.
+1. (Optional) Have a **wildcard certificate** for that domain if you decide to
+ serve Pages under HTTPS.
+1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md)
+ so that your users don't have to bring their own.
+
+### DNS configuration
+
+GitLab Pages expect to run on their own virtual host. In your DNS server/provider
+you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
+host that GitLab runs. For example, an entry would look like this:
+
+```
+*.example.io. 1800 IN A 1.2.3.4
+```
+
+where `example.io` is the domain under which GitLab Pages will be served
+and `1.2.3.4` is the IP address of your GitLab instance.
+
+> **Note:**
+You should not use the GitLab domain to serve user pages. For more information
+see the [security section](#security).
+
+[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "https://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
+ gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ gitlab_pages['external_https'] = '1.1.1.2:443'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url "http://example.io"
+ nginx['listen_addresses'] = ['1.1.1.1']
+ pages_nginx['enable'] = false
+ gitlab_pages['external_http'] = '1.1.1.2:80'
+ ```
+
+ where `1.1.1.1` is the primary IP address that GitLab is listening to and
+ `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Place the certificate and key inside `/etc/gitlab/ssl`
+1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
+
+ ```ruby
+ pages_external_url 'https://example.io'
+
+ pages_nginx['redirect_http_to_https'] = true
+ pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
+ pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
+ ```
+
+ where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key,
+ respectively.
+
+1. [Reconfigure GitLab][reconfigure]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_external_url 'http://example.io'
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['pages_path'] = "/mnt/storage/pages"
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
+## Set maximum pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+## Changelog
+
+GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features
+where added, like custom CNAME and TLS support, and many more are likely to
+come. Below is a brief changelog. If no changes were introduced or a version is
+missing from the changelog, assume that the documentation is the same as the
+latest previous version.
+
+---
+
+**GitLab 8.17 ([documentation][8-17-docs])**
+
+- GitLab Pages were ported to Community Edition in GitLab 8.17.
+- Documentation was refactored to be more modular and easy to follow.
+
+**GitLab 8.5 ([documentation][8-5-docs])**
+
+- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the
+ recommended way to set up GitLab Pages.
+- The [NGINX configs][] have changed to reflect this change. So make sure to
+ update them.
+- Custom CNAME and TLS certificates support.
+- Documentation was moved to one place.
+
+**GitLab 8.3 ([documentation][8-3-docs])**
+
+- GitLab Pages feature was introduced.
+
+[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md
+[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md
+[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md
+[backup]: ../raketasks/backup_restore.md
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
new file mode 100644
index 00000000000..d4468b99992
--- /dev/null
+++ b/doc/administration/pages/source.md
@@ -0,0 +1,323 @@
+# GitLab Pages administration for source installations
+
+This is the documentation for configuring a GitLab Pages when you have installed
+GitLab from source and not using the Omnibus packages.
+
+You are encouraged to read the [Omnibus documentation](index.md) as it provides
+some invaluable information to the configuration of GitLab Pages. 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 GitLab
+Pages to the latest supported version.
+
+## Overview
+
+[Read the Omnibus overview section.](index.md#overview)
+
+## Prerequisites
+
+[Read the Omnibus prerequisites section.](index.md#prerequisites)
+
+## Configuration
+
+Depending on your needs, you can install GitLab Pages in four different ways.
+
+### Option 1. Custom domains with HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` and `external_https` to the secondary IP on which the pages
+ daemon will listen for connections:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+
+ external_http: 1.1.1.2:80
+ external_https: 1.1.1.2:443
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`,
+ `external_http` and `external_https` settings that you set above respectively.
+ The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+ of the `example.io` domain:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 2. Custom domains without HTTPS support
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` and `http://page.com` | no | yes | no | yes |
+
+Pages enabled, daemon is enabled AND pages has external IP support enabled.
+In that case, the pages daemon is running, NGINX still proxies requests to
+the daemon but the daemon is also able to receive requests from the outside
+world. Custom domains and TLS are supported.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Edit `gitlab.yml` to look like the example below. You need to change the
+ `host` to the FQDN under which GitLab Pages will be served. Set
+ `external_http` to the secondary IP on which the pages daemon will listen
+ for connections:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+
+ external_http: 1.1.1.2:80
+ ```
+
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` and `-listen-http` must match the `host` and `external_http`
+ settings that you set above respectively:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
+ `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
+ listens to.
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 3. Wildcard HTTPS domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `https://page.example.io` | yes | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+1. In `gitlab.yml`, set the port to `443` and https to `true`:
+
+ ```bash
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 443
+ https: true
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+### Option 4. Wildcard HTTP domain without custom domains
+
+| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP |
+| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
+| `http://page.example.io` | no | no | no | no |
+
+Pages enabled, daemon is enabled and NGINX will proxy all requests to the
+daemon. Pages daemon doesn't listen to the outside world.
+
+1. Install the Pages daemon:
+
+ ```
+ cd /home/git
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
+ cd gitlab-pages
+ sudo -u git -H git checkout v0.2.4
+ sudo -u git -H make
+ ```
+
+1. Go to the GitLab installation directory:
+
+ ```bash
+ cd /home/git/gitlab
+ ```
+
+1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
+ the `host` to the FQDN under which GitLab Pages will be served:
+
+ ```yaml
+ ## GitLab Pages
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ # path: shared/pages
+
+ host: example.io
+ port: 80
+ https: false
+ ```
+
+1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ ```
+
+ Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+
+1. Restart NGINX
+1. [Restart GitLab][restart]
+
+## NGINX caveats
+
+>**Note:**
+The following information applies only for installations from source.
+
+Be extra careful when setting up the domain name in the NGINX config. You must
+not remove the backslashes.
+
+If your GitLab pages domain is `example.io`, replace:
+
+```bash
+server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+```
+
+with:
+
+```
+server_name ~^.*\.example\.io$;
+```
+
+If you are using a subdomain, make sure to escape all dots (`.`) except from
+the first one with a backslash (\). For example `pages.example.io` would be:
+
+```
+server_name ~^.*\.pages\.example\.io$;
+```
+
+## Change storage path
+
+Follow the steps below to change the default path where GitLab Pages' contents
+are stored.
+
+1. Pages are stored by default in `/home/git/gitlab/shared/pages`.
+ If you wish to store them in another location you must set it up in
+ `gitlab.yml` under the `pages` section:
+
+ ```yaml
+ pages:
+ enabled: true
+ # The location where pages are stored (default: shared/pages).
+ path: /mnt/storage/pages
+ ```
+
+1. [Restart GitLab][restart]
+
+## Set maximum Pages size
+
+The maximum size of the unpacked archive per project can be configured in the
+Admin area under the Application settings in the **Maximum size of pages (MB)**.
+The default is 100MB.
+
+## Backup
+
+Pages are part of the [regular backup][backup] so there is nothing to configure.
+
+## Security
+
+You should strongly consider running GitLab pages under a different hostname
+than GitLab to prevent XSS attacks.
+
+[backup]: ../raketasks/backup_restore.md
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx
+[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md
+[pages-userguide]: ../../user/project/pages/index.md
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../administration/restart_gitlab.md#installations-from-source
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index 22f10489a6c..3b8c716eff5 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -315,7 +315,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
## Done!
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure GitLab.
---
diff --git a/doc/api/README.md b/doc/api/README.md
index 20f28e8d30e..b334ca46caf 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -49,6 +49,7 @@ following locations:
- [Todos](todos.md)
- [Users](users.md)
- [Validate CI configuration](ci/lint.md)
+- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
### Internal CI API
diff --git a/doc/api/groups.md b/doc/api/groups.md
index f7807390e68..a3a43ca7f1c 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -25,7 +25,15 @@ GET /groups
"id": 1,
"name": "Foobar Group",
"path": "foo-bar",
- "description": "An interesting group"
+ "description": "An interesting group",
+ "visibility_level": 20,
+ "lfs_enabled": true,
+ "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg",
+ "web_url": "http://localhost:3000/groups/foo-bar",
+ "request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": null
}
]
```
@@ -98,15 +106,7 @@ Example response:
"id": 5,
"name": "Experimental",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
+ "kind": "group"
},
"avatar_url": null,
"star_count": 1,
@@ -149,6 +149,9 @@ Example response:
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/twitter",
"request_access_enabled": false,
+ "full_name": "Twitter",
+ "full_path": "twitter",
+ "parent_id": null,
"projects": [
{
"id": 7,
@@ -179,15 +182,7 @@ Example response:
"id": 4,
"name": "Twitter",
"path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
@@ -226,15 +221,7 @@ Example response:
"id": 4,
"name": "Twitter",
"path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
@@ -275,15 +262,7 @@ Example response:
"id": 5,
"name": "H5bp",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:26.621Z",
- "updated_at": "2016-06-17T07:47:26.621Z",
- "description": "Id consequatur rem vel qui doloremque saepe.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
+ "kind": "group"
},
"avatar_url": null,
"star_count": 0,
@@ -323,6 +302,7 @@ Parameters:
- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group
- `request_access_enabled` (optional) - Allow users to request member access.
+- `parent_id` (optional) - The parent group id for creating nested group.
## Transfer project to group
@@ -372,6 +352,9 @@ Example response:
"avatar_url": null,
"web_url": "http://gitlab.example.com/groups/h5bp",
"request_access_enabled": false,
+ "full_name": "Foobar Group",
+ "full_path": "foo-bar",
+ "parent_id": null,
"projects": [
{
"id": 9,
@@ -401,15 +384,7 @@ Example response:
"id": 5,
"name": "Experimental",
"path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
+ "kind": "group"
},
"avatar_url": null,
"star_count": 1,
diff --git a/doc/api/issues.md b/doc/api/issues.md
index b276d1ad918..7c0a444d4fa 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -181,7 +181,6 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
-GET /projects/:id/issues?iid=42
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 7b005591545..6ee377125d6 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all
-GET /projects/:id/merge_requests?iid=42
-GET /projects/:id/merge_requests?iid[]=42&iid[]=43
+GET /projects/:id/merge_requests?iids[]=42&iids[]=43
```
Parameters:
@@ -351,16 +350,17 @@ PUT /projects/:id/merge_requests/:merge_request_id
| --------- | ---- | -------- | ----------- |
| `id` | string | yes | The ID of a project |
| `merge_request_id` | integer | yes | The ID of a merge request |
-| `source_branch` | string | yes | The source branch |
-| `target_branch` | string | yes | The target branch |
-| `title` | string | yes | Title of MR |
+| `target_branch` | string | no | The target branch |
+| `title` | string | no | Title of MR |
| `assignee_id` | integer | no | Assignee user ID |
| `description` | string | no | Description of MR |
-| `target_project_id` | integer | no | The target project (numeric id) |
+| `state_event` | string | no | New state (close/reopen) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
+Must include at least one non-required attribute from above.
+
```json
{
"id": 1,
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 5ef5e3f5744..eab532af594 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -57,7 +57,7 @@ Once you have the authorization code you can request an `access_token` using the
```
parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
-RestClient.post 'http://localhost:3000/oauth/token', parameters
+RestClient.post 'http://gitlab.example.com/oauth/token', parameters
# The response will be
{
@@ -77,13 +77,13 @@ You can now make requests to the API with the access token returned.
The access token allows you to make requests to the API on a behalf of a user.
```
-GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN
+GET https://gitlab.example.com/api/v3/user?access_token=OAUTH-TOKEN
```
Or you can put the token to the Authorization header:
```
-curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/user
```
## Resource Owner Password Credentials
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index c6685f54a9d..404876f6237 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -51,7 +51,6 @@ Parameters:
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
- "expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/example/example/snippets/1"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 122075bbd11..bad238f57d7 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -72,13 +72,10 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
@@ -125,13 +122,10 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 4,
"name": "Brightbox",
- "owner_id": 1,
"path": "brightbox",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"permissions": {
"project_access": {
@@ -210,13 +204,10 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"archived": false,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
@@ -260,13 +251,10 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 4,
"name": "Brightbox",
- "owner_id": 1,
"path": "brightbox",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"permissions": {
"project_access": {
@@ -398,13 +386,10 @@ Parameters:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"permissions": {
"project_access": {
@@ -642,7 +627,6 @@ Parameters:
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
@@ -676,7 +660,6 @@ Parameters:
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
@@ -709,7 +692,6 @@ Parameters:
| `snippets_enabled` | boolean | no | Enable snippets for this project |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
-| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
@@ -782,13 +764,10 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
@@ -850,13 +829,10 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
@@ -924,13 +900,10 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"permissions": {
"project_access": {
@@ -1009,13 +982,10 @@ Example response:
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
- "created_at": "2013-09-30T13:46:02Z",
- "description": "",
"id": 3,
"name": "Diaspora",
- "owner_id": 1,
"path": "diaspora",
- "updated_at": "2013-09-30T13:46:02Z"
+ "kind": "group"
},
"permissions": {
"project_access": {
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 8a6baed5987..dbb3c1113e8 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -53,7 +53,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
@@ -82,7 +82,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
@@ -113,14 +113,14 @@ DELETE /projects/:id/repository/files
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch_name": "master"
}
```
diff --git a/doc/api/services.md b/doc/api/services.md
index 1466b8189b0..fba5da6587d 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -808,5 +808,5 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
-[jira-doc]: ../project_services/jira.md
+[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
diff --git a/doc/api/settings.md b/doc/api/settings.md
index f86c7cc2f94..ca6b9347877 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -46,7 +46,8 @@ Example response:
"koding_enabled": false,
"koding_url": null,
"plantuml_enabled": false,
- "plantuml_url": null
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
@@ -84,6 +85,7 @@ PUT /application/settings
| `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources |
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
+| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -118,6 +120,7 @@ Example response:
"koding_enabled": false,
"koding_url": null,
"plantuml_enabled": false,
- "plantuml_url": null
+ "plantuml_url": null,
+ "terminal_max_session_time": 0
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 28b6c7bd491..ed3469521fc 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -216,7 +216,7 @@ Parameters:
## User creation
-Creates a new user. Note only administrators can create new users.
+Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority).
```
POST /users
@@ -225,7 +225,8 @@ POST /users
Parameters:
- `email` (required) - Email
-- `password` (required) - Password
+- `password` (optional) - Password
+- `reset_password` (optional) - Send user password reset link - true or false(default)
- `username` (required) - Username
- `name` (required) - Name
- `skype` (optional) - Skype ID
@@ -271,6 +272,7 @@ Parameters:
- `can_create_group` (optional) - User can create groups - true or false
- `external` (optional) - Flags the user as external - true or false(default)
+On password update, user will be forced to change it upon next login.
Note, at the moment this method does only return a `404` error,
even in cases where a `409` (Conflict) would be more appropriate,
e.g. when renaming the email address to some existing one.
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
new file mode 100644
index 00000000000..7cb83a337f2
--- /dev/null
+++ b/doc/api/v3_to_v4.md
@@ -0,0 +1,15 @@
+# V3 to V4 version
+
+Our V4 API version is currently available as *Beta*! It means that V3
+will still be supported and remain unchanged for now, but be aware that the following
+changes are in V4:
+
+### Changes
+
+- Removed `/projects/:search` (use: `/projects?search=x`)
+- `iid` filter has been removed from `projects/:id/issues`
+- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
+- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
+- Project snippets do not return deprecated field `expires_at`
+- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
+- Status 409 returned for POST `project/:id/members` when a member already exists
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 630207ffa09..4028a5efa9e 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,6 +1,6 @@
# Auto deploy
-> [Introduced][mr-8135] in GitLab 8.15.
+> [Introduced][mr-8135] in GitLab 8.15. Currently requires a [Public project][project-settings].
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
@@ -33,8 +33,9 @@ enable [Kubernetes service][kubernetes-service].
created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
-[project-services]: ../../project_services/project_services.md
+[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
+[project-services]: ../../user/project/integrations/project_services.md
[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
-[kubernetes-service]: ../../project_services/kubernetes.md
+[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 98cd29c9567..cb62ed723f0 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -1,7 +1,6 @@
# Introduction to environments and deployments
->**Note:**
-Introduced in GitLab 8.9.
+> Introduced in GitLab 8.9.
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
@@ -242,7 +241,7 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open
+the [Kubernetes service][kubernetes-service], GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service documentation.
@@ -297,7 +296,7 @@ deploy_review:
- echo "Deploy a review app"
environment:
name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.review.example.com
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
except:
@@ -318,15 +317,15 @@ also contain `/`, or other characters that would be invalid in a domain name or
URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
environment can get a specific and distinct URL for each branch. In this case,
given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
-like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up
+like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
-`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+`https://$CI_BUILD_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
here because it is guaranteed to be unique, but if you're using a workflow like
[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
environment names to be more closely based on the branch name - the example
-above would give you an URL like `https://100-do-the-thing.review.example.com`
+above would give you an URL like `https://100-do-the-thing.example.com`
Last but not least, we tell the job to run [`only`][only] on branches
[`except`][only] master.
@@ -443,6 +442,57 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
+### Go directly from source files to public pages on the environment
+
+> Introduced in GitLab 8.17.
+
+To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places:
+
+| In the diff for a merge request, comparison or commit | In the file view |
+| ------ | ------ |
+| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) |
+
+To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map.
+
+A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website).
+
+This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com):
+
+```yaml
+# Team data
+- source: 'data/team.yml' # data/team.yml
+ public: 'team/' # team/
+
+# Blogposts
+- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+# HTML files
+- source: /source\/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+# Other files
+- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+```
+
+Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys:
+
+- `source`
+ - a string, starting and ending with `'`, for an exact match
+ - a regular expression, starting and ending with `/`, for a pattern match
+ - The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
+ - Can include capture groups denoted by `()` that can be referred to in the `public` path.
+ - Slashes (`/`) can, but don't have to, be escaped as `\/`.
+ - Literal periods (`.`) should be escaped as `\.`.
+- `public`
+ - a string, starting and ending with `'`.
+ - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`.
+
+The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate.
+
+In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`.
+
---
We now have a full development cycle, where our app is tested, built, deployed
@@ -566,7 +616,7 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
-[kubernetes-service]: ../project_services/kubernetes.md]
+[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index 82ffb841729..5eeec92d976 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -235,7 +235,11 @@ cache:
before_script:
# Install composer dependencies
-- curl --silent --show-error https://getcomposer.org/installer | php
+- wget https://composer.github.io/installer.sig -O - -q | tr -d '\n' > installer.sig
+- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+- php -r "if (hash_file('SHA384', 'composer-setup.php') === file_get_contents('installer.sig')) { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
+- php composer-setup.php
+- php -r "unlink('composer-setup.php'); unlink('installer.sig');"
- php composer.phar install
...
diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png
new file mode 100644
index 00000000000..f4fe99046f0
--- /dev/null
+++ b/doc/ci/img/view_on_env_blob.png
Binary files differ
diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png
new file mode 100644
index 00000000000..47ddb40bdc1
--- /dev/null
+++ b/doc/ci/img/view_on_env_mr.png
Binary files differ
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index f91b9d350f7..2c7c3ef3c18 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -75,5 +75,5 @@ respective link in the [Pipelines settings] page.
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
-[runners]: runners/READM
+[runners]: runners/README.html
[pipelines settings]: ../user/project/pipelines/settings.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c40cdd55ea5..1104edaabe9 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -217,7 +217,7 @@ builds, you should explicitly enable the **Builds Emails** service under your
project's settings.
For more information read the
-[Builds emails service documentation](../../project_services/builds_emails.md).
+[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
## Examples
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d3b9611b02e..49fca884f35 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -157,14 +157,14 @@ Once you set them, they will be available for all subsequent builds.
>**Note:**
This feature requires GitLab CI 8.15 or higher.
-[Project services](../../project_services/project_services.md) that are
+[Project services](../../user/project/integrations/project_services.md) that are
responsible for deployment configuration may define their own variables that
are set in the build environment. These variables are only defined for
[deployment builds](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
An example project service that defines deployment variables is
-[Kubernetes Service](../../project_services/kubernetes.md).
+[Kubernetes Service](../../user/project/integrations/kubernetes.md).
## Debug tracing
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index f11257be5c3..cd492d16747 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -319,6 +319,7 @@ job_name:
| before_script | no | Override a set of commands that are executed before build |
| after_script | no | Override a set of commands that are executed after build |
| environment | no | Defines a name of environment to which deployment is done by this build |
+| coverage | no | Define code coverage settings for a given job |
### script
@@ -993,6 +994,23 @@ job:
- execute this after my script
```
+### coverage
+
+`coverage` allows you to configure how code coverage will be extracted from the
+job output.
+
+Regular expressions are the only valid kind of value expected here. So, using
+surrounding `/` is mandatory in order to consistently and explicitly represent
+a regular expression string. You must escape special characters if you want to
+match them literally.
+
+A simple example:
+
+```yaml
+job1:
+ coverage: /Code coverage: \d+\.\d+/
+```
+
## Git Strategy
> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
@@ -1281,6 +1299,35 @@ with an API call.
[Read more in the triggers documentation.](../triggers/README.md)
+### pages
+
+`pages` is a special job that is used to upload static content to GitLab that
+can be used to serve your website. It has a special syntax, so the two
+requirements below must be met:
+
+1. Any static content must be placed under a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+The example below simply moves all files from the root of the project to the
+`public/` directory. The `.public` workaround is so `cp` doesn't also copy
+`public/` to itself in an infinite loop:
+
+```
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+Read more on [GitLab Pages user documentation](../../pages/README.md).
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index f79bd23dc90..ba47998de49 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -23,6 +23,69 @@ some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
+#### How to build a new feature with Vue.js
+**Components, Stores and Services**
+
+In some features implemented with Vue.js, like the [issue board][issue-boards]
+or [environments table][environments-table]
+you can find a clear separation of concerns:
+
+```
+new_feature
+├── components
+│ └── component.js.es6
+│ └── ...
+├── store
+│ └── new_feature_store.js.es6
+├── service
+│ └── new_feature_service.js.es6
+├── new_feature_bundle.js.es6
+```
+_For consistency purposes, we recommend you to follow the same structure._
+
+Let's look into each of them:
+
+**A `*_bundle.js` file**
+
+This is the index file of your new feature. This is where the root Vue instance
+of the new feature should be.
+
+Don't forget to follow [these steps.][page_specific_javascript]
+
+**A folder for Components**
+
+This folder holds all components that are specific of this new feature.
+If you need to use or create a component that will probably be used somewhere
+else, please refer to `vue_shared/components`.
+
+A good thumb rule to know when you should create a component is to think if
+it will be reusable elsewhere.
+
+For example, tables are used in a quite amount of places across GitLab, a table
+would be a good fit for a component.
+On the other hand, a table cell used only in on table, would not be a good use
+of this pattern.
+
+You can read more about components in Vue.js site, [Component System][component-system]
+
+**A folder for the Store**
+
+The Store is a simple object that allows us to manage the state in a single
+source of truth.
+
+The concept we are trying to follow is better explained by Vue documentation
+itself, please read this guide: [State Management][state-management]
+
+**A folder for the Service**
+
+The Service is used only to communicate with the server.
+It does not store or manipulate any data.
+We use [vue-resource][vue-resource-repo] to
+communicate with the server.
+
+The [issue boards service][issue-boards-service]
+is a good example of this pattern.
+
## Performance
### Resources
@@ -187,23 +250,17 @@ information.
### Running frontend tests
-`rake teaspoon` runs the frontend-only (JavaScript) tests.
+`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
-- `rake teaspoon:fixtures` (re-)generates fixtures
-- `rake teaspoon:tests` actually executes the tests
+- `rake karma:fixtures` (re-)generates fixtures
+- `rake karma:tests` actually executes the tests
-As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
+As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time).
-If you need to debug your tests and/or application code while they're
-running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
-in your browser, open DevTools, and run tests for individual files by clicking
-on them. This is also much faster than setting up and running tests from the
-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`.
+files. These will not be touched by `rake karma:fixtures`.
## Design Patterns
@@ -260,54 +317,20 @@ gl.MyThing = MyThing;
For our currently-supported browsers, see our [requirements][requirements].
-[rails]: http://rubyonrails.org/
-[haml]: http://haml.info/
-[hamlit]: https://github.com/k0kubun/hamlit
-[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
-[scss]: http://sass-lang.com/
-[es6]: https://babeljs.io/
-[sprockets]: https://github.com/rails/sprockets
-[jquery]: https://jquery.com/
-[vue]: http://vuejs.org/
-[vue-docs]: http://vuejs.org/guide/index.html
-[web-page-test]: http://www.webpagetest.org/
-[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
-[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
-[browser-diet]: https://browserdiet.com/
-[d3]: https://d3js.org/
-[chartjs]: http://www.chartjs.org/
-[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
-[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
-[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
-[observatory-cli]: https://github.com/mozilla/http-observatory-cli
-[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
-[secure_headers]: https://github.com/twitter/secureheaders
-[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
-[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
-[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
-[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
-[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
-[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
-[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
-[github-eng-sri]: http://githubengineering.com/subresource-integrity/
-[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
-[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
-[scss-style-guide]: scss_styleguide.md
-[requirements]: ../install/requirements.md#supported-web-browsers
## Gotchas
### Spec errors due to use of ES6 features in `.js` files
-If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
-thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually,
-you may have included `ES6`-style JavaScript in files that don't have the
-`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
-you're working in (`git mv <file.js> <file.js.es6>`).
+If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
+thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
+you may have included `ES6`-style JavaScript in files that don't have the
+`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
+you're working in (`git mv <file.js> <file.js.es6>`).
### Spec errors due to use of unsupported JavaScript
-Similar errors will be thrown if you're using JavaScript features not yet
+Similar errors will be thrown if you're using JavaScript features not yet
supported by our test runner's version of webkit, whether or not you've updated
the file extension. Examples of unsupported JavaScript features are:
@@ -322,20 +345,20 @@ the file extension. Examples of unsupported JavaScript features are:
- Symbol/Symbol.iterator
- Spread
-Until these are polyfilled or transpiled appropriately, they should not be used.
-Please update this list with additional unsupported features or when any of
+Until these are polyfilled or transpiled appropriately, they should not be used.
+Please update this list with additional unsupported features or when any of
these are made usable.
### Spec errors due to JavaScript not enabled
-If, as a result of a change you've made, a feature now depends on JavaScript to
+If, as a result of a change you've made, a feature now depends on JavaScript to
run correctly, you need to make sure a JavaScript web driver is enabled when
-specs are run. If you don't you'll see vague error messages from the spec
-runner, and an explosion of vague console errors in the HTML snapshot.
+specs are run. If you don't you'll see vague error messages from the spec
+runner, and an explosion of vague console errors in the HTML snapshot.
-To enable a JavaScript driver in an `rspec` test, add `js: true` to the
-individual spec or the context block containing multiple specs that need
-JavaScript enabled:
+To enable a JavaScript driver in an `rspec` test, add `js: true` to the
+individual spec or the context block containing multiple specs that need
+JavaScript enabled:
```ruby
@@ -354,8 +377,8 @@ describe "Admin::AbuseReports", js: true do
end
```
-In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
-file for the failing spec, add the `@javascript` flag above the Scenario:
+In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
+file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
@@ -368,3 +391,46 @@ Scenario: Developer can approve merge request
Then I should see approved merge request "Bug NS-04"
```
+
+
+[rails]: http://rubyonrails.org/
+[haml]: http://haml.info/
+[hamlit]: https://github.com/k0kubun/hamlit
+[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
+[scss]: http://sass-lang.com/
+[es6]: https://babeljs.io/
+[sprockets]: https://github.com/rails/sprockets
+[jquery]: https://jquery.com/
+[vue]: http://vuejs.org/
+[vue-docs]: http://vuejs.org/guide/index.html
+[web-page-test]: http://www.webpagetest.org/
+[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
+[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
+[browser-diet]: https://browserdiet.com/
+[d3]: https://d3js.org/
+[chartjs]: http://www.chartjs.org/
+[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
+[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[observatory-cli]: https://github.com/mozilla/http-observatory-cli
+[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
+[secure_headers]: https://github.com/twitter/secureheaders
+[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
+[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
+[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
+[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
+[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
+[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
+[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+[github-eng-sri]: http://githubengineering.com/subresource-integrity/
+[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
+[scss-style-guide]: scss_styleguide.md
+[requirements]: ../install/requirements.md#supported-web-browsers
+[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
+[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
+[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
+[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
+[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
+[vue-resource-repo]: https://github.com/pagekit/vue-resource
+[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 827db7e99b8..dcd978c4cd3 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing.
In order to run the test you can use the following commands:
- `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite
-- `rake teaspoon` to run the teaspoon test suite
+- `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests
-Note: Both `rake spinach` and `rake spec` takes significant time to pass.
+Note: Both `rake spinach` and `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running
-a single test or directory related to your changes. After you submit merge request
-CI will run full test suite for you. Green CI status in the merge request means
-full test suite is passed.
+a single test or directory related to your changes. After you submit merge request
+CI will run full test suite for you. Green CI status in the merge request means
+full test suite is passed.
Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
files it can find, also the ones in `/tmp`
diff --git a/doc/development/testing.md b/doc/development/testing.md
index dbea6b9c9aa..761847b2bab 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement.
## JavaScript
-GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on
-the command line via `bundle exec teaspoon`, or via a web browser at
-`http://localhost:3000/teaspoon` when the Rails server is running.
+GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
+the command line via `bundle exec karma`.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
-[Teaspoon]: https://github.com/modeset/teaspoon
+[Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine
## RSpec
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index 2d1d504202c..df6ac452300 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -20,8 +20,8 @@ The content section contains a header and the content itself. The header describ
available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
-You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
-along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository][gitlab-map-graffle]
+along with [PDF][gitlab-map-pdf] and [PNG][gitlab-map-png] exports.
### Adding new tab to header navigation
@@ -104,4 +104,4 @@ Do not use both green and blue button in one form.
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
-[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md
index 903e54bf9dc..5dae4bcc905 100644
--- a/doc/development/ux_guide/animation.md
+++ b/doc/development/ux_guide/animation.md
@@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net](
### Hover
-Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect.
+Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect.
View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 1b19587a0b8..18d0647c798 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -96,6 +96,20 @@ Since secondary buttons only have a border on their resting state, their hover a
| Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) |
| Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) |
+### Placement
+
+When there are a group of buttons in a dialog or a form, we need to be consistent with the placement.
+
+#### Dismissive actions on the left
+The dismissive action returns the user to the previous state.
+
+> Example: Cancel
+
+#### Affirmative actions on the right
+Affirmative actions continue to progress towards the user goal that triggered the dialog or form.
+
+> Example: Submit, Ok, Delete
+
---
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index 3b075ff5fc0..2a531193adf 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -25,6 +25,8 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
+>**Note:** If you clone your project via an URL that contains special characters, make sure that they are URL-encoded.
+
### Go into a project, directory or file to work in it
```
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
index 26df4c0a734..aaa1a39313d 100644
--- a/doc/gitlab-basics/img/profile_settings.png
+++ b/doc/gitlab-basics/img/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
index 6a1430d9663..7ebb8973ef0 100644
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index 239f5f301ec..2d2fd8cb380 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -4,3 +4,6 @@
- [Requirements](requirements.md)
- [Structure](structure.md)
- [Database MySQL](database_mysql.md)
+- [Digital Ocean and Docker](digitaloceandocker.md)
+- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
+- [All installation methods](https://about.gitlab.com/installation/)
diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md
new file mode 100644
index 00000000000..820060a489b
--- /dev/null
+++ b/doc/install/digitaloceandocker.md
@@ -0,0 +1,136 @@
+# Digital Ocean and Docker
+
+## Initial setup
+
+In this guide you'll configure a Digital Ocean droplet and set up Docker
+locally on either macOS or Linux.
+
+### On macOS
+
+#### Install Docker Toolbox
+
+1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox)
+
+### On Linux
+
+#### Install Docker Engine
+
+1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/)
+
+#### Install Docker Machine
+
+1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/)
+
+_The rest of the steps are identical for macOS and Linux_
+
+### Create new docker host
+
+1. Login to Digital Ocean
+1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens
+
+
+This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host.
+
+**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance**
+
++ RAM: 4GB
++ Name: `gitlab-test-env-do`
++ Driver: `digitalocean`
+
+
+**Set the DO token** - Replace the string below with your generated token
+
+```
+export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248
+```
+
+**Create the machine**
+
+```
+docker-machine create \
+ --driver digitalocean \
+ --digitalocean-access-token=$DOTOKEN \
+ --digitalocean-size "4gb" \
+ gitlab-test-env-do
+```
+
++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/
+
+
+### Creating GitLab test instance
+
+
+#### Connect your shell to the new machine
+
+
+In this example we'll create a GitLab EE 8.10.8 instance.
+
+
+First connect the docker client to the docker host you created previously.
+
+```
+eval "$(docker-machine env gitlab-test-env-do)"
+```
+
+You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host
+
+
+#### Create new GitLab container
+
++ HTTP port: `8888`
++ SSH port: `2222`
+ + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG `
++ Hostname: IP of docker host
++ Container name: `gitlab-test-8.10`
++ GitLab version: **EE** `8.10.8-ee.0`
+
+##### Setup container settings
+
+```
+export SSH_PORT=2222
+export HTTP_PORT=8888
+export VERSION=8.10.8-ee.0
+export NAME=gitlab-test-8.10
+```
+
+##### Create container
+```
+docker run --detach \
+--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \
+--hostname $(docker-machine ip gitlab-test-env-do) \
+-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \
+--name $NAME \
+gitlab/gitlab-ee:$VERSION
+```
+
+#### Connect to the GitLab container
+
+##### Retrieve the docker host IP
+
+```
+docker-machine ip gitlab-test-env-do
+# example output: 192.168.151.134
+```
+
+
++ Browse to: http://192.168.151.134:8888/
+
+
+##### Execute interactive shell/edit configuration
+
+
+```
+docker exec -it $NAME /bin/bash
+```
+
+```
+# example commands
+root@192:/# vi /etc/gitlab/gitlab.rb
+root@192:/# gitlab-ctl reconfigure
+```
+
+#### Resources
+
++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/)
++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/)
++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 425c5d93efb..355179960b3 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -124,7 +124,7 @@ Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
- echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+ echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-17-stable gitlab
-**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-17-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -313,6 +313,9 @@ sudo usermod -aG redis git
# Change the permissions of the directory where CI artifacts are stored
sudo chmod -R u+rwX shared/artifacts/
+ # Change the permissions of the directory where GitLab Pages are stored
+ sudo chmod -R ug+rwX shared/pages/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -484,6 +487,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat
# or else sudo rm -f /etc/nginx/sites-enabled/default
sudo editor /etc/nginx/sites-available/gitlab
+If you intend to enable GitLab pages, there is a separate Nginx config you need
+to use. Read all about the needed configuration at the
+[GitLab Pages administration guide](../administration/pages/index.md).
+
**Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details.
### Test Configuration
diff --git a/doc/integration/README.md b/doc/integration/README.md
index e97430feb57..22bdf33443d 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](../project_services/jira.md) Integrate with the JIRA issue tracker
+- [JIRA](../user/project/integrations/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, Azure and Authentiq ID
@@ -18,17 +18,14 @@ See the documentation below for details on how to configure these services.
- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
-GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
-
-[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
-
+> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
## Project services
Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
Pivotal Tracker, and Slack are available in the form of a [Project Service][].
-[Project Service]: ../project_services/project_services.md
+[Project Service]: ../user/project/integrations/project_services.md
## SSL certificate errors
@@ -64,3 +61,5 @@ After that restart GitLab with:
```bash
sudo gitlab-ctl restart
```
+
+[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index e5247082a89..212b4854dd7 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -80,10 +80,13 @@ from step 5.
1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console
page from step 5.
-1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md)
-for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be an Auth0 icon below the regular sign in
form. Click the icon to begin the authentication process. Auth0 will ask the
user to sign in and authorize the GitLab application. If everything goes well
the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 48dddf7df44..5e3e9f5ab77 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -78,6 +78,10 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/cas.md b/doc/integration/cas.md
index e34e306f9ac..f757edf0bc2 100644
--- a/doc/integration/cas.md
+++ b/doc/integration/cas.md
@@ -58,8 +58,11 @@ To enable the CAS OmniAuth provider you must register your application with your
1. Save the configuration file.
-1. Run `gitlab-ctl reconfigure` for the omnibus package.
-
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a CAS tab in the sign in form.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index 40d93aef2a9..f8370cd349e 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -53,6 +53,11 @@ To enable the Crowd OmniAuth provider you must register your application with Cr
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be a Crowd tab in the sign in form.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
-On the sign in page there should now be a Crowd tab in the sign in form. \ No newline at end of file
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 8d2c6351fb8..265c891cf83 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -18,9 +18,9 @@ The configuration is done via a project's **Services**.
To enable an external issue tracker you must configure the appropriate **Service**.
Visit the links below for details:
-- [Redmine](../project_services/redmine.md)
-- [Jira](../project_services/jira.md)
-- [Bugzilla](../project_services/bugzilla.md)
+- [Redmine](../user/project/integrations/redmine.md)
+- [Jira](../user/project/integrations/jira.md)
+- [Bugzilla](../user/project/integrations/bugzilla.md)
### Service Template
@@ -28,4 +28,4 @@ To save you the hassle from configuring each project's service individually,
GitLab provides the ability to set Service Templates which can then be
overridden in each project's settings.
-Read more on [Services Templates](../project_services/services_templates.md).
+Read more on [Services Templates](../user/project/integrations/services_templates.md).
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
index 77bb75cbfca..a67de23b17b 100644
--- a/doc/integration/facebook.md
+++ b/doc/integration/facebook.md
@@ -92,6 +92,10 @@ something else descriptive.
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 479c697b933..cea85f073cc 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -2,7 +2,7 @@
Import projects from GitHub and login to your GitLab instance with your GitHub account.
-To enable the GitHub OmniAuth provider you must register your application with GitHub.
+To enable the GitHub OmniAuth provider you must register your application with GitHub.
GitHub will generate an application ID and secret key for you to use.
1. Sign in to GitHub.
@@ -22,7 +22,7 @@ GitHub will generate an application ID and secret key for you to use.
- Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
1. Select "Register application".
-1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
+1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
Keep this page open as you continue configuration.
![GitHub app](img/github_app.png)
@@ -49,7 +49,7 @@ GitHub will generate an application ID and secret key for you to use.
For omnibus package:
For GitHub.com:
-
+
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -60,9 +60,9 @@ GitHub will generate an application ID and secret key for you to use.
}
]
```
-
+
For GitHub Enterprise:
-
+
```ruby
gitlab_rails['omniauth_providers'] = [
{
@@ -101,10 +101,14 @@ GitHub will generate an application ID and secret key for you to use.
1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7.
-1. Save the configuration file and run `sudo gitlab-ctl reconfigure`.
+1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
-On the sign in page there should now be a GitHub icon below the regular sign in form.
-Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
+On the sign in page there should now be a GitHub icon below the regular sign in form.
+Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 6d8f3912ede..eec40a9b8f1 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -2,7 +2,7 @@
Import projects from GitLab.com and login to your GitLab instance with your GitLab.com account.
-To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
+To enable the GitLab.com OmniAuth provider you must register your application with GitLab.com.
GitLab.com will generate an application ID and secret key for you to use.
1. Sign in to GitLab.com
@@ -26,8 +26,8 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select "Submit".
-1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
- Keep this page open as you continue configuration.
+1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot).
+ Keep this page open as you continue configuration.
![GitLab app](img/gitlab_app.png)
1. On your GitLab server, open the configuration file.
@@ -77,8 +77,12 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
-On the sign in page there should now be a GitLab.com icon below the regular sign in form.
-Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
+On the sign in page there should now be a GitLab.com icon below the regular sign in form.
+Click the icon to begin the authentication process. GitLab.com will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to your GitLab instance and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 82978b68a34..1e7ad90c5a8 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -74,7 +74,8 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Google icon below the regular sign in form. Click the icon to begin the authentication process. Google will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
@@ -87,3 +88,6 @@ At this point, when users first try to authenticate to your GitLab installation
1. Select 'Consent screen' in the left menu. (See steps 1, 4 and 5 above for instructions on how to get here if you closed your window).
1. Scroll down until you find "Product Name". Change the product name to something more descriptive.
1. Add any additional information as you wish - homepage, logo, privacy policy, etc. None of this is required, but it may help your users.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index e2f136bcc35..b6923f74e28 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,3 +1 @@
-# GitLab JIRA integration
-
-This document was moved to [project_services/jira](../project_services/jira.md).
+This document was moved to [integrations/jira](../user/project/integrations/jira.md).
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 4a242c321aa..7a809eddac0 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -109,7 +109,8 @@ in your SAML IdP:
1. Change the value of `issuer` to a unique name, which will identify the application
to the IdP.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
in `issuer`.
@@ -314,3 +315,6 @@ For this you need take the following into account:
Make sure that one of the above described scenarios is valid, or the requests will
fail with one of the mentioned errors.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 696c1011eeb..e0fc1bb801f 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -70,10 +70,9 @@ gitlab_rails['omniauth_providers'] = [
]
```
-1. Save changes and reconfigure gitlab:
-```
-sudo gitlab-ctl reconfigure
-```
+
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a "Sign in with: Shibboleth" icon below the regular sign in form. Click the icon to begin the authentication process. You will be redirected to IdP server (Depends on your Shibboleth module configuration). If everything goes well the user will be returned to GitLab and will be signed in.
@@ -122,4 +121,7 @@ you will not get a shibboleth session!
RequestHeader set X_FORWARDED_PROTO 'https'
RequestHeader set X-Forwarded-Ssl on
-``` \ No newline at end of file
+```
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index abbea09f22f..d0976b6201e 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -74,6 +74,10 @@ To enable the Twitter OmniAuth provider you must register your application with
1. Save the configuration file.
-1. Restart GitLab for the changes to take effect.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
+
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/pages/README.md b/doc/pages/README.md
new file mode 100644
index 00000000000..c9715eed598
--- /dev/null
+++ b/doc/pages/README.md
@@ -0,0 +1 @@
+This document was moved to [user/project/pages](../user/project/pages/index.md).
diff --git a/doc/pages/administration.md b/doc/pages/administration.md
new file mode 100644
index 00000000000..4eb3bb32c77
--- /dev/null
+++ b/doc/pages/administration.md
@@ -0,0 +1 @@
+This document was moved to [administration/pages](../administration/pages/index.md).
diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md
index 51668128c62..5b171080c72 100644
--- a/doc/project_services/bamboo.md
+++ b/doc/project_services/bamboo.md
@@ -1,60 +1 @@
-# Atlassian Bamboo CI Service
-
-GitLab provides integration with Atlassian Bamboo for continuous integration.
-When configured, pushes to a project will trigger a build in Bamboo automatically.
-Merge requests will also display CI status showing whether the build is pending,
-failed, or completed successfully. It also provides a link to the Bamboo build
-page for more information.
-
-Bamboo doesn't quite provide the same features as a traditional build system when
-it comes to accepting webhooks and commit data. There are a few things that
-need to be configured in a Bamboo build plan before GitLab can integrate.
-
-## Setup
-
-### Complete these steps in Bamboo:
-
-1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
-dropdown.
-1. Select the 'Triggers' tab.
-1. Click 'Add trigger'.
-1. Enter a description such as 'GitLab trigger'
-1. Choose 'Repository triggers the build when changes are committed'
-1. Check one or more repositories checkboxes
-1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
-whitelist of IP addresses that are allowed to trigger Bamboo builds.
-1. Save the trigger.
-1. In the left pane, select a build stage. If you have multiple build stages
-you want to select the last stage that contains the git checkout task.
-1. Select the 'Miscellaneous' tab.
-1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
-in the 'Labels' box.
-1. Save
-
-Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
-service in GitLab
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure to trigger builds.
-1. Select 'Settings' in the top navigation.
-1. Select 'Services' in the left navigation.
-1. Click 'Atlassian Bamboo CI'
-1. Select the 'Active' checkbox.
-1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
-1. Enter the build key from your Bamboo build plan. Build keys are a short,
-all capital letter, identifier that is unique. It will be something like PR-BLD
-1. If necessary, enter username and password for a Bamboo user that has
-access to trigger the build plan. Leave these fields blank if you do not require
-authentication.
-1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
-will actually trigger a build in Bamboo.
-
-## Troubleshooting
-
-If builds are not triggered, these are a couple of things to keep in mind.
-
-1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
-IP addresses'.
-1. Remember that GitLab only triggers builds on push events. A commit via the
-web interface will not trigger CI currently.
+This document was moved to [user/project/integrations/bamboo.md](../user/project/integrations/bamboo.md).
diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md
index 215ed6fe9cc..e67055d5616 100644
--- a/doc/project_services/bugzilla.md
+++ b/doc/project_services/bugzilla.md
@@ -1,17 +1 @@
-# Bugzilla Service
-
-Go to your project's **Settings > Services > Bugzilla** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
-
-Once you have configured and enabled Bugzilla:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Bugzilla product page
-- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
+This document was moved to [user/project/integrations/bugzilla.md](../user/project/integrations/bugzilla.md).
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
index af0b1a287c7..ee54d865225 100644
--- a/doc/project_services/builds_emails.md
+++ b/doc/project_services/builds_emails.md
@@ -1,16 +1 @@
-## Enabling build emails
-
-To receive e-mail notifications about the result status of your builds, visit
-your project's **Settings > Services > Builds emails** and activate the service.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
-
----
-
-![Builds emails service settings](img/builds_emails_service.png)
+This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
index 2f9f36f962e..a2e831ada34 100644
--- a/doc/project_services/emails_on_push.md
+++ b/doc/project_services/emails_on_push.md
@@ -1,17 +1 @@
-## Enabling emails on push
-
-To receive email notifications for every change that is pushed to the project, visit
-your project's **Settings > Services > Emails on push** and activate the service.
-
-In the _Recipients_ area, provide a list of emails separated by commas.
-
-You can configure any of the following settings depending on your preference.
-
-+ **Push events** - Email will be triggered when a push event is recieved
-+ **Tag push events** - Email will be triggered when a tag is created and pushed
-+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
-+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
-
----
-
-![Email on push service settings](img/emails_on_push_service.png)
+This document was moved to [user/project/integrations/emails_on_push.md](../user/project/integrations/emails_on_push.md).
diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md
index 021a93a288f..4ae9f6c6b2e 100644
--- a/doc/project_services/hipchat.md
+++ b/doc/project_services/hipchat.md
@@ -1,54 +1 @@
-# Atlassian HipChat
-
-GitLab provides a way to send HipChat notifications upon a number of events,
-such as when a user pushes code, creates a branch or tag, adds a comment, and
-creates a merge request.
-
-## Setup
-
-GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
-not supported at this time. Note the differences between v1 and v2 tokens:
-
-HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
-token is allowed to send messages to *any* room.
-
-HipChat v2 API has tokens that are can be created using the Integrations tab
-in the Group or Room admin page. By design, these are lightweight tokens that
-allow GitLab to send messages only to *one* room.
-
-### Complete these steps in HipChat:
-
-1. Go to: https://admin.hipchat.com/admin
-1. Click on "Group Admin" -> "Integrations".
-1. Find "Build Your Own!" and click "Create".
-1. Select the desired room, name the integration "GitLab", and click "Create".
-1. In the "Send messages to this room by posting this URL" column, you should
-see a URL in the format:
-
-```
- https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
-```
-
-HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
-service in GitLab.
-
-### Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "HipChat".
-1. Select the "Active" checkbox.
-1. Insert the `token` field from the URL into the `Token` field on the Web page.
-1. Insert the `room` field from the URL into the `Room` field on the Web page.
-1. Save or optionally click "Test Settings".
-
-## Troubleshooting
-
-If you do not see notifications, make sure you are using a HipChat v2 API
-token, not a v1 token.
-
-Note that the v2 token is tied to a specific room. If you want to be able to
-specify arbitrary rooms, you can create an API token for a specific user in
-HipChat under "Account settings" and "API access". Use the `XXX` value under
-`auth_token=XXX`.
+This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md).
diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png
deleted file mode 100644
index 9dbbed03833..00000000000
--- a/doc/project_services/img/builds_emails_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png
deleted file mode 100644
index a62e4b792f9..00000000000
--- a/doc/project_services/img/mattermost_config_help.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/slack_setup.png b/doc/project_services/img/slack_setup.png
deleted file mode 100644
index f69817f2b78..00000000000
--- a/doc/project_services/img/slack_setup.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md
index 25c0c3ad2a6..7f0850dcc24 100644
--- a/doc/project_services/irker.md
+++ b/doc/project_services/irker.md
@@ -1,51 +1 @@
-# Irker IRC Gateway
-
-GitLab provides a way to push update messages to an Irker server. When
-configured, pushes to a project will trigger the service to send data directly
-to the Irker server.
-
-See the project homepage for further info: https://gitlab.com/esr/irker
-
-## Needed setup
-
-You will first need an Irker daemon. You can download the Irker code from its
-repository on https://gitlab.com/esr/irker:
-
-```
-git clone https://gitlab.com/esr/irker.git
-```
-
-Once you have downloaded the code, you can run the python script named `irkerd`.
-This script is the gateway script, it acts both as an IRC client, for sending
-messages to an IRC server obviously, and as a TCP server, for receiving messages
-from the GitLab service.
-
-If the Irker server runs on the same machine, you are done. If not, you will
-need to follow the firsts steps of the next section.
-
-## Complete these steps in GitLab:
-
-1. Navigate to the project you want to configure for notifications.
-1. Select "Settings" in the top navigation.
-1. Select "Services" in the left navigation.
-1. Click "Irker".
-1. Select the "Active" checkbox.
-1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
-in the `Server host` field on the Web page
-1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
-`Server port` field on the Web page.
-1. Optional: if `Default IRC URI` is set, it has to be in the format
-`irc[s]://domain.name` and will be prepend to each and every channel provided
-by the user which is not a full URI.
-1. Specify the recipients (e.g. #channel1, user1, etc.)
-1. Save or optionally click "Test Settings".
-
-## Note on Irker recipients
-
-Irker accepts channel names of the form `chan` and `#chan`, both for the
-`#chan` channel. If you want to send messages in query, you will need to add
-`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
-case, `Aorimn` is treated as a nick and no more as a channel name.
-
-Irker can also join password-protected channels. Users need to append
-`?key=thesecretpassword` to the chan name.
+This document was moved to [user/project/integrations/irker.md](../user/project/integrations/irker.md).
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index 390066c9989..63614feba82 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1,208 +1 @@
-# 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.
-
-## Configuration
-
-In order to enable the JIRA service in GitLab, you need to first configure the
-project in JIRA and then enter the correct values in GitLab.
-
-### 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
-
->**Notes:**
-- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
- higher is required.
-- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
- the configuration options you have to enter. If you are using an older version,
- [follow this documentation][jira-repo-docs].
-
-To enable JIRA integration in a project, navigate to your project's
-**Services ➔ JIRA** and 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. E.g., `https://jira.example.com`. |
-| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
-| `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)
-
----
-
-## 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
-
-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, e.g., `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 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 PROJECT-1`
-- `Closes PROJECT-1`
-- `Fixes PROJECT-1`
-
-where `PROJECT-1` is the issue ID of the JIRA project.
-
-### JIRA issue closing example
-
-Let's consider the following example:
-
-1. For the project named `PROJECT` in JIRA, we implemented a new feature
- and created a merge request in GitLab.
-1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
- in GitLab contains the improvement
-1. In the merge request description we use the issue closing trigger
- `Closes PROJECT-7`.
-1. Once the merge request is merged, the JIRA issue will be automatically closed
- with a comment and an associated link to the commit that resolved the issue.
-
----
-
-In the following screenshot you can see what the link references to the JIRA
-issue look like.
-
-![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.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 closes JIRA issue](img/jira_service_close_issue.png)
-
----
-
-![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
-
-## Troubleshooting
-
-If things don't work as expected that's usually because you have configured
-incorrectly the JIRA-GitLab integration.
-
-### 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.
-JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
-
-### GitLab is unable to close a ticket
-
-Make sure the `Transition ID` you set within the JIRA settings matches the one
-your project needs to close a ticket.
-
-[services-templates]: ../project_services/services_templates.md
-[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
+This document was moved to [user/project/integrations/jira.md](../user/project/integrations/jira.md).
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
index 99aa9e44bdb..0497a13c2b7 100644
--- a/doc/project_services/kubernetes.md
+++ b/doc/project_services/kubernetes.md
@@ -1,63 +1 @@
-# GitLab Kubernetes / OpenShift integration
-
-GitLab can be configured to interact with Kubernetes, or other systems using the
-Kubernetes API (such as OpenShift).
-
-Each project can be configured to connect to a different Kubernetes cluster, see
-the [configuration](#configuration) section.
-
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see the [Services Templates](services_templates.md) document.
-
-## Configuration
-
-![Kubernetes configuration settings](img/kubernetes_configuration.png)
-
-The Kubernetes service takes the following arguments:
-
-1. Kubernetes namespace
-1. API URL
-1. Service token
-1. Custom CA bundle
-
-The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
-exposes several APIs - we want the "base" URL that is common to all of them,
-e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-
-GitLab authenticates against Kubernetes using service tokens, which are
-scoped to a particular `namespace`. If you don't have a service token yet,
-you can follow the
-[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
-to create one. You can also view or create service tokens in the
-[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
-`Config -> Secrets`.
-
-Fill in the service token and namespace according to the values you just got.
-If the API is using a self-signed TLS certificate, you'll also need to include
-the `ca.crt` contents as the `Custom CA bundle`.
-
-## Deployment variables
-
-The Kubernetes service exposes following
-[deployment variables](../ci/variables/README.md#deployment-variables) in the
-GitLab CI build environment:
-
-- `KUBE_URL` - equal to the API URL
-- `KUBE_TOKEN`
-- `KUBE_NAMESPACE`
-- `KUBE_CA_PEM` - only if a custom CA bundle was specified
-
-## Web terminals
-
->**NOTE:**
-Added in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is currently limited to the first container in the
-first pod of your environment.
-
-When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals)
-support to your environments. This is based on the `exec` functionality found in
-Docker and Kubernetes, so you get a new shell session within your existing
-containers. To use this integration, you should deploy to Kubernetes using
-the deployment variables above, ensuring any pods you create are labelled with
-`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
+This document was moved to [user/project/integrations/kubernetes.md](../user/project/integrations/kubernetes.md).
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
index fbc7dfeee6d..554a028853e 100644
--- a/doc/project_services/mattermost.md
+++ b/doc/project_services/mattermost.md
@@ -1,45 +1 @@
-# Mattermost Notifications Service
-
-## On Mattermost
-
-To enable Mattermost integration you must create an incoming webhook integration:
-
-1. Sign in to your Mattermost instance
-1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
-1. Choose a display name, description and channel, those can be overridden on GitLab
-1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
-
-There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
-it on https://mattermost.example/admin_console/integrations/custom.
-
-Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
-
-## On GitLab
-
-After you set up Mattermost, it's time to set up GitLab.
-
-Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- Wiki page
-
-Bellow each of these event checkboxes, you will have an input field to insert
-which Mattermost channel you want to send that event message, with `#town-square`
-being the default. The hash sign is optional.
-
-At the end, fill in your Mattermost details:
-
-| Field | Description |
-| ----- | ----------- |
-| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
-| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
-
-
-![Mattermost configuration](img/mattermost_configuration.png)
+This document was moved to [user/project/integrations/mattermost.md](../user/project/integrations/mattermost.md).
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 67cb88104c1..7c238b5dc37 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -1,163 +1 @@
-# Mattermost slash commands
-
-> Introduced in GitLab 8.14
-
-Mattermost commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in Mattermost.
-
-## Prerequisites
-
-Mattermost 3.4 and up is required.
-
-If you have the Omnibus GitLab package installed, Mattermost is already bundled
-in it. All you have to do is configure it. Read more in the
-[Omnibus GitLab Mattermost documentation][omnimmdocs].
-
-## Automated Configuration
-
-If Mattermost is installed on the same server as GitLab, the configuration process can be
-done for you by GitLab.
-
-Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
-
-## Manual Configuration
-
-The configuration consists of two parts. First you need to enable the slash
-commands in Mattermost and then enable the service in GitLab.
-
-### Step 1. Enable custom slash commands in Mattermost
-
-This step is only required when using a source install, omnibus installs will be
-preconfigured with the right settings.
-
-The first thing to do in Mattermost is to enable custom slash commands from
-the administrator console.
-
-1. Log in with an account that has admin privileges and navigate to the system
- console.
-
- ![Mattermost go to console](img/mattermost_goto_console.png)
-
- ---
-
-1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
- **Enable custom integrations to override usernames**, and **Override
- custom integrations to override profile picture icons** to true
-
- ![Mattermost console](img/mattermost_console_integrations.png)
-
- ---
-
-1. Click **Save** at the bottom to save the changes.
-
-### Step 2. Open the Mattermost slash commands service in GitLab
-
-1. Open a new tab for GitLab and go to your project's settings
- **Services ➔ Mattermost command**. A screen will appear with all the values you
- need to copy in Mattermost as described in the next step. Leave the window open.
-
- >**Note:**
- GitLab will propose some values for the Mattermost settings. The only one
- required to copy-paste as-is is the **Request URL**, all the others are just
- suggestions.
-
- ![Mattermost setup instructions](img/mattermost_config_help.png)
-
- ---
-
-1. Proceed to the next step and create a slash command in Mattermost with the
- above values.
-
-### Step 3. Create a new custom slash command in Mattermost
-
-Now that you have enabled custom slash commands in Mattermost and opened
-the Mattermost slash commands service in GitLab, it's time to copy these values
-in a new slash command.
-
-1. Back to Mattermost, under your team page settings, you should see the
- **Integrations** option.
-
- ![Mattermost team integrations](img/mattermost_team_integrations.png)
-
- ---
-
-1. Go to the **Slash Commands** integration and add a new one by clicking the
- **Add Slash Command** button.
-
- ![Mattermost add command](img/mattermost_add_slash_command.png)
-
- ---
-
-1. Fill in the options for the custom command as described in
- [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
-
- >**Note:**
- If you plan on connecting multiple projects, pick a slash command trigger
- word that relates to your projects such as `/gitlab-project-name` or even
- just `/project-name`. Only use `/gitlab` if you will only connect a single
- project to your Mattermost team.
-
- ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
-
-1. After you setup all the values, copy the token (we will use it below) and
- click **Done**.
-
- ![Mattermost slash command token](img/mattermost_slash_command_token.png)
-
-### Step 4. Copy the Mattermost token into the Mattermost slash command service
-
-1. In GitLab, paste the Mattermost token you copied in the previous step and
- check the **Active** checkbox.
-
- ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
-
-1. Click **Save changes** for the changes to take effect.
-
----
-
-You are now set to start using slash commands in Mattermost that talk to the
-GitLab project you configured.
-
-## Authorizing Mattermost to interact with GitLab
-
-The first time a user will interact with the newly created slash commands,
-Mattermost will trigger an authorization process.
-
-![Mattermost bot authorize](img/mattermost_bot_auth.png)
-
-This will connect your Mattermost user with your GitLab user. You can
-see all authorized chat accounts in your profile's page under **Chat**.
-
-When the authorization process is complete, you can start interacting with
-GitLab using the Mattermost commands.
-
-## Available slash commands
-
-The available slash commands are:
-
-| Command | Description | Example |
-| ------- | ----------- | ------- |
-| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
-| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
-| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
-
-To see a list of available commands to interact with GitLab, type the
-trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
-
-![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
-
-## Permissions
-
-The permissions to run the [available commands](#available-commands) derive from
-the [permissions you have on the project](../user/permissions.md#project).
-
-## Further reading
-
-- [Mattermost slash commands documentation][mmslashdocs]
-- [Omnibus GitLab Mattermost][omnimmdocs]
-
-
-[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
-[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
-[ciyaml]: ../ci/yaml/README.md
+This document was moved to [user/project/integrations/mattermost_slash_commands.md](../user/project/integrations/mattermost_slash_commands.md).
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 547d855d777..2c555c4edae 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -1,59 +1 @@
-# Project Services
-
-Project services allow you to integrate GitLab with other applications. Below
-is list of the currently supported ones.
-
-You can find these within GitLab in the Services page under Project Settings if
-you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in
-adding functionality to GitLab. For example there is also a service that can
-send an email every time someone pushes new commits.
-
-Because GitLab is open source we can ship with the code and tests for all
-plugins. This allows the community to keep the plugins up to date so that they
-always work in newer GitLab versions.
-
-For an overview of what projects services are available without logging in,
-please see the [project_services directory][projects-code].
-
-[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
-
-Click on the service links to see
-further configuration instructions and details. Contributions are welcome.
-
-## Services
-
-| Service | Description |
-| ------- | ----------- |
-| Asana | Asana - Teamwork without email |
-| Assembla | Project Management Software (Source Commits Endpoint) |
-| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
-| Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
-| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
-| Campfire | Simple web-based real-time group chat |
-| Custom Issue Tracker | Custom issue tracker |
-| Drone CI | Continuous Integration platform built on Docker, written in Go |
-| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
-| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
-| Flowdock | Flowdock is a collaboration web app for technical teams |
-| 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](jira.md) | JIRA issue tracker |
-| JetBrains TeamCity CI | A continuous integration and build server |
-| [Kubernetes](kubernetes.md) | A containerized deployment service |
-| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
-| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
-| [Slack Notifications](slack.md) | Receive event notifications in Slack |
-| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
-| PivotalTracker | Project Management Software (Source Commits Endpoint) |
-| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
-| [Redmine](redmine.md) | Redmine issue tracker |
-
-## Services Templates
-
-Services templates is a way to set some predefined values in the Service of
-your liking which will then be pre-filled on each project's Service.
-
-Read more about [Services Templates in this document](services_templates.md).
+This document was moved to [user/project/integrations/project_services.md](../user/project/integrations/project_services.md).
diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md
index b9830ea7c38..6010aa4dc75 100644
--- a/doc/project_services/redmine.md
+++ b/doc/project_services/redmine.md
@@ -1,21 +1 @@
-# Redmine Service
-
-Go to your project's **Settings > Services > Redmine** and fill in the required
-details as described in the table below.
-
-| Field | Description |
-| ----- | ----------- |
-| `description` | A name for the issue tracker (to differentiate between instances, for example) |
-| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
-| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
-| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
-
-Once you have configured and enabled Redmine:
-
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- Redmine issue index
-- clicking **New issue** on the project dashboard creates a new Redmine issue
-
-As an example, below is a configuration for a project named gitlab-ci.
-
-![Redmine configuration](img/redmine_configuration.png)
+This document was moved to [user/project/integrations/redmine.md](../user/project/integrations/redmine.md).
diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md
index be6d13b6d2b..8905d667c5a 100644
--- a/doc/project_services/services_templates.md
+++ b/doc/project_services/services_templates.md
@@ -1,25 +1 @@
-# Services Templates
-
-A GitLab administrator can add a service template that sets a default for each
-project. This makes it much easier to configure individual projects.
-
-After the template is created, the template details will be pre-filled on a
-project's Service page.
-
-## Enable a Service template
-
-In GitLab's Admin area, navigate to **Service Templates** and choose the
-service template you wish to create.
-
-For example, in the image below you can see Redmine.
-
-![Redmine service template](img/services_templates_redmine_example.png)
-
----
-
-**NOTE:** For each project, you will still need to configure the issue tracking
-URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
-by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
-the project settings, and GitLab would automatically update the URL configured
-in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
-must be configured directly within the project's **Services** settings.
+This document was moved to [user/project/integrations/services_templates.md](../user/project/integrations/services_templates.md).
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 0b682b43810..1d3f98705e3 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,50 +1 @@
-# Slack Notifications Service
-
-## On Slack
-
-To enable Slack integration you must create an incoming webhook integration on
-Slack:
-
-1. [Sign in to Slack](https://slack.com/signin)
-1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-1. Choose the channel name you want to send notifications to.
-1. Click **Add Incoming WebHooks Integration**
-1. Copy the **Webhook URL**, we'll need this later for GitLab.
-
-## On GitLab
-
-After you set up Slack, it's time to set up GitLab.
-
-Go to your project's **Settings > Services > Slack Notifications** and you will see a
-checkbox with the following events that can be triggered:
-
-- Push
-- Issue
-- Merge request
-- Note
-- Tag push
-- Build
-- Wiki page
-
-Bellow each of these event checkboxes, you will have an input field to insert
-which Slack channel you want to send that event message, with `#general`
-being the default. Enter your preferred channel **without** the hash sign (`#`).
-
-At the end, fill in your Slack details:
-
-| Field | Description |
-| ----- | ----------- |
-| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
-| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
-
-After you are all done, click **Save changes** for the changes to take effect.
-
->**Note:**
-You can set "branch,pushed,Compare changes" as highlight words on your Slack
-profile settings, so that you can be aware of new commits when somebody pushes
-them.
-
-![Slack configuration](img/slack_configuration.png)
-
-[slackhook]: https://my.slack.com/services/new/incoming-webhook
+This document was moved to [user/project/integrations/slack.md](../user/project/integrations/slack.md).
diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md
index d9ff573d185..9554c8decc8 100644
--- a/doc/project_services/slack_slash_commands.md
+++ b/doc/project_services/slack_slash_commands.md
@@ -1,23 +1 @@
-# Slack slash commands
-
-> Introduced in GitLab 8.15
-
-Slack commands give users an extra interface to perform common operations
-from the chat environment. This allows one to, for example, create an issue as
-soon as the idea was discussed in chat.
-For all available commands try the help subcommand, for example: `/gitlab help`,
-all review the [full list of commands](../integration/chat_commands.md).
-
-## Prerequisites
-
-A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you.
-
-## Configuration
-
-First, navigate to the Slack Slash commands service page, found at your project's
-**Settings** > **Services**, and you find the instructions there:
-
- ![Slack setup instructions](img/slack_setup.png)
-
-Once you've followed the instructions, mark the service as active and insert the token
-you've received from Slack. After saving the service you are good to go!
+This document was moved to [user/project/integrations/slack_slash_commands.md](../user/project/integrations/slack_slash_commands.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6b4db71b44..0fb69d63dbe 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -84,6 +84,29 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
+## Exclude specific directories from the backup
+
+You can choose what should be backed up by adding the environment variable `SKIP`.
+The available options are:
+
+* `db`
+* `uploads` (attachments)
+* `repositories`
+* `builds` (CI build output logs)
+* `artifacts` (CI build artifacts)
+* `lfs` (LFS objects)
+* `pages` (pages content)
+
+Use a comma to specify several options at the same time:
+
+```
+# use this command if you've installed GitLab with the Omnibus package
+sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+
+# if you've installed GitLab from source
+sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
+```
+
## Upload backups to remote (cloud) storage
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md
index f9a46193547..fee49cc27cc 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -7,7 +7,7 @@ This command will enable the namespaces feature introduced in v4.0. It will move
Note:
- Because the **repository location will change**, you will need to **update all your git URLs** to point to the new location.
-- Username can be changed at [Profile / Account](/profile/account)
+- Username can be changed at **Profile ➔ Account**.
**Example:**
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index bb46aebf4b5..faabc53ce72 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -2,7 +2,7 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
-With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
+With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
@@ -10,4 +10,4 @@ Because Webhook requests are made by the GitLab server itself, these have comple
If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
-To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file
+To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9803937fcf9..9e391d647a8 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -4,10 +4,12 @@ Git is a distributed version control system, which means you can work locally
but you can also share or "push" your changes to other servers.
Before you can push your changes to a GitLab server
you need a secure communication channel for sharing information.
-GitLab uses Public-key or asymmetric cryptography
-which encrypts a communication channel by locking it with your "private key"
-and allows trusted parties to unlock it with your "public key".
-If someone does not have your public key they cannot access the unencrypted message.
+
+The SSH protocol provides this security and allows you to authenticate to the
+GitLab remote server without supplying your username or password each time.
+
+For a more detailed explanation of how the SSH protocol works, we advise you to
+read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process).
## Locating an existing SSH key pair
diff --git a/doc/university/README.md b/doc/university/README.md
index 12727e9d56f..e9f14703789 100644
--- a/doc/university/README.md
+++ b/doc/university/README.md
@@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
-1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html)
+1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/)
#### 2.2. GitLab Issues
@@ -189,10 +189,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
#### 3.9. Integrations
1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
-1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ce/user/project/integrations/jira.html)
1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html)
-1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
-1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Bamboo with GitLab](https://docs.gitlab.com/ce/user/project/integrations/bamboo.html)
+1. [How to Integrate Slack with GitLab](https://docs.gitlab.com/ce/user/project/integrations/slack.html)
1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 20e7ea1987f..979a1c5d310 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -573,7 +573,7 @@ A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building softwa
### Webhooks
-A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
+A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integrations/webhooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient.
### Wiki
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index 6e415e4d219..ca538ef6dc3 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -172,7 +172,7 @@ Move on to understanding some of GitLab's more advanced features. You can make u
- Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings
- Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
- Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html)
-- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html)
+- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/)
- Get to know the GitLab Codebase by reading through the source code:
- Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index 1ee615432aa..3ed601625cf 100755
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -5,4 +5,4 @@
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)
+6. Contact Us at `subscribers@gitlab.com`
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index 35afe73708f..9e38df26b6a 100755
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -389,4 +389,4 @@ GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/gui
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)
+Contact Us at `subscribers@gitlab.com`
diff --git a/doc/update/2.6-to-3.0.md b/doc/update/2.6-to-3.0.md
index fb70eaacbc9..97cd277b424 100644
--- a/doc/update/2.6-to-3.0.md
+++ b/doc/update/2.6-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.6 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.6-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/2.9-to-3.0.md b/doc/update/2.9-to-3.0.md
index ce46b57c09a..a890aa885d5 100644
--- a/doc/update/2.9-to-3.0.md
+++ b/doc/update/2.9-to-3.0.md
@@ -1,5 +1,5 @@
# From 2.9 to 3.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/2.9-to-3.0.md) for the most up to date instructions.*
## 1. Stop server & resque
diff --git a/doc/update/3.0-to-3.1.md b/doc/update/3.0-to-3.1.md
index 6ac83f3b60d..e32508745a2 100644
--- a/doc/update/3.0-to-3.1.md
+++ b/doc/update/3.0-to-3.1.md
@@ -1,5 +1,5 @@
# From 3.0 to 3.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.0-to-3.1.md) for the most up to date instructions.*
**IMPORTANT!**
diff --git a/doc/update/3.1-to-4.0.md b/doc/update/3.1-to-4.0.md
index df53ed6de83..b370464390e 100644
--- a/doc/update/3.1-to-4.0.md
+++ b/doc/update/3.1-to-4.0.md
@@ -1,5 +1,5 @@
# From 3.1 to 4.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/3.1-to-4.0.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index c66c6dd0fd8..7124424bb60 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -1,5 +1,5 @@
# From 4.0 to 4.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.0-to-4.1.md) for the most up to date instructions.*
## Important changes
diff --git a/doc/update/4.1-to-4.2.md b/doc/update/4.1-to-4.2.md
index 97367c5f347..8ed5b333a2e 100644
--- a/doc/update/4.1-to-4.2.md
+++ b/doc/update/4.1-to-4.2.md
@@ -1,5 +1,5 @@
# From 4.1 to 4.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.1-to-4.2.md) for the most up to date instructions.*
## 1. Stop server & Resque
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index 7654f4a0131..1ec39218ba8 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -1,5 +1,5 @@
# From 4.2 to 5.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/4.2-to-5.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index c19a819ab5a..9c9950fb2c6 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -1,5 +1,5 @@
# From 5.0 to 5.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.0-to-5.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md
index 625fcc33852..2aab47d2d7c 100644
--- a/doc/update/5.1-to-5.2.md
+++ b/doc/update/5.1-to-5.2.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.2.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md
index 547d453914c..e80f1b89c63 100644
--- a/doc/update/5.1-to-5.4.md
+++ b/doc/update/5.1-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.1 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-5.4.md) for the most up to date instructions.*
Also works starting from 5.2.
diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md
index c992c69678e..1ee175383da 100644
--- a/doc/update/5.1-to-6.0.md
+++ b/doc/update/5.1-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.1 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.1-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index fe8990b6843..2ae50510f63 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -1,5 +1,5 @@
# From 5.2 to 5.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.2-to-5.3.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index 5f82ad7d444..842e3bb6791 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -1,5 +1,5 @@
# From 5.3 to 5.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.3-to-5.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md
index f0fee634322..44715984f0c 100644
--- a/doc/update/5.4-to-6.0.md
+++ b/doc/update/5.4-to-6.0.md
@@ -1,5 +1,5 @@
# From 5.4 to 6.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/5.4-to-6.0.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md
index 409faf30902..0c672abeb05 100644
--- a/doc/update/6.0-to-6.1.md
+++ b/doc/update/6.0-to-6.1.md
@@ -1,5 +1,5 @@
# From 6.0 to 6.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.0-to-6.1.md) for the most up to date instructions.*
## Warning
diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md
index 150c7ae1c83..d3760cf0619 100644
--- a/doc/update/6.1-to-6.2.md
+++ b/doc/update/6.1-to-6.2.md
@@ -1,5 +1,5 @@
# From 6.1 to 6.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.1-to-6.2.md) for the most up to date instructions.*
**You should update to 6.1 before installing 6.2 so all the necessary conversions are run.**
diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md
index b96dfb8add7..91105de2e29 100644
--- a/doc/update/6.2-to-6.3.md
+++ b/doc/update/6.2-to-6.3.md
@@ -1,5 +1,5 @@
# From 6.2 to 6.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.2-to-6.3.md) for the most up to date instructions.*
**Requires version: 6.1 or 6.2.**
diff --git a/doc/update/6.3-to-6.4.md b/doc/update/6.3-to-6.4.md
index 37028be055f..20b58ed8b25 100644
--- a/doc/update/6.3-to-6.4.md
+++ b/doc/update/6.3-to-6.4.md
@@ -1,5 +1,5 @@
# From 6.3 to 6.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.3-to-6.4.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.4-to-6.5.md b/doc/update/6.4-to-6.5.md
index 982381a4db0..5ee0f040b5d 100644
--- a/doc/update/6.4-to-6.5.md
+++ b/doc/update/6.4-to-6.5.md
@@ -1,5 +1,5 @@
# From 6.4 to 6.5
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.4-to-6.5.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.5-to-6.6.md b/doc/update/6.5-to-6.6.md
index bbed2b30215..fa3712f83ad 100644
--- a/doc/update/6.5-to-6.6.md
+++ b/doc/update/6.5-to-6.6.md
@@ -1,5 +1,5 @@
# From 6.5 to 6.6
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.5-to-6.6.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 8e82942a1a0..9c85ed091c5 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -1,5 +1,5 @@
# From 6.6 to 6.7
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.6-to-6.7.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.7-to-6.8.md b/doc/update/6.7-to-6.8.md
index 4fb90639f16..687c1265d9b 100644
--- a/doc/update/6.7-to-6.8.md
+++ b/doc/update/6.7-to-6.8.md
@@ -1,5 +1,5 @@
# From 6.7 to 6.8
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.7-to-6.8.md) for the most up to date instructions.*
## 0. Backup
diff --git a/doc/update/6.8-to-6.9.md b/doc/update/6.8-to-6.9.md
index b9b8b63f652..0205b0c896a 100644
--- a/doc/update/6.8-to-6.9.md
+++ b/doc/update/6.8-to-6.9.md
@@ -1,5 +1,5 @@
# From 6.8 to 6.9
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.8-to-6.9.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 5352fd52f93..4b6e3989893 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -1,5 +1,5 @@
# From 6.9 to 7.0
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.9-to-7.0.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index f170a0021b7..1e39fe47ef9 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -1,5 +1,5 @@
# From 6.x or 7.x to 7.14
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.*
This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14.
@@ -222,7 +222,7 @@ If all items are green, then congratulations upgrade complete!
When using Google omniauth login, changes of the Google account required.
Ensure that `Contacts API` and the `Google+ API` are enabled in the [Google Developers Console](https://console.developers.google.com/).
-More details can be found at the [integration documentation](../../../master/doc/integration/google.md).
+More details can be found at the [integration documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/integration/google.md).
## 12. Optional optimizations for GitLab setups with MySQL databases
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index 71f39c44077..c717affebd3 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -1,5 +1,5 @@
# From 7.0 to 7.1
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.0-to-7.1.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md
index 88cb63d7d41..d01f8528e14 100644
--- a/doc/update/7.1-to-7.2.md
+++ b/doc/update/7.1-to-7.2.md
@@ -1,5 +1,5 @@
# From 7.1 to 7.2
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.1-to-7.2.md) for the most up to date instructions.*
## Editable labels
diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md
index 18f77d6396e..0e91e682175 100644
--- a/doc/update/7.2-to-7.3.md
+++ b/doc/update/7.2-to-7.3.md
@@ -1,5 +1,5 @@
# From 7.2 to 7.3
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.2-to-7.3.md) for the most up to date instructions.*
### 0. Backup
diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md
index 53e739c06fb..4df9127dd5f 100644
--- a/doc/update/7.3-to-7.4.md
+++ b/doc/update/7.3-to-7.4.md
@@ -1,5 +1,5 @@
# From 7.3 to 7.4
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update/7.3-to-7.4.md) for the most up to date instructions.*
### 0. Stop server
diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md
new file mode 100644
index 00000000000..1808232c59a
--- /dev/null
+++ b/doc/update/8.16-to-8.17.md
@@ -0,0 +1,239 @@
+# From 8.16 to 8.17
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-17-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 8-17-stable-ee
+```
+
+### 5. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v4.1.1
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/8-16-stable:config/gitlab.yml.example origin/8-17-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/8-16-stable:lib/support/nginx/gitlab-ssl origin/8-17-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-16-stable:lib/support/nginx/gitlab origin/8-17-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 9. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.16)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.15 to 8.16](8.15-to-8.16.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
index 9336dee7451..2459f913583 100644
--- a/doc/user/account/security.md
+++ b/doc/user/account/security.md
@@ -1 +1 @@
-This document was moved to [profile](../profile/index.md#security).
+This document was moved to [profile](../profile/index.md).
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index 4b540473a6e..603b826e7f2 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -1,5 +1,20 @@
# Sign-up restrictions
+You can block email addresses of specific domains, or whitelist only some
+specifc domains via the **Application Settings** in the Admin area.
+
+>**Note**: These restrictions are only applied during sign-up. An admin is
+able to add add a user through the admin panel with a disallowed domain. Also
+note that the users can change their email addresses after signup to
+disallowed domains.
+
+## Whitelist email domains
+
+> [Introduced][ce-598] in GitLab 7.11.0
+
+You can restrict users to only signup using email addresses matching the given
+domains list.
+
## Blacklist email domains
> [Introduced][ce-5259] in GitLab 8.10.
@@ -9,13 +24,16 @@ from creating an account on your GitLab server. This is particularly useful to
prevent spam. Disposable email addresses are usually used by malicious users to
create dummy accounts and spam issues.
+## Settings
+
This feature can be activated via the **Application Settings** in the Admin area,
and you have the option of entering the list manually, or uploading a file with
the list.
-The blacklist accepts wildcards, so you can use `*.test.com` to block every
-`test.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains
-should be separated by a whitespace, semicolon, comma, or a new line.
+Both whitelist and blacklist accept wildcards, so for example, you can use
+`*.company.com` to accept every `company.com` subdomain, or `*.io` to block all
+domains ending in `.io`. Domains should be separated by a whitespace,
+semicolon, comma, or a new line.
![Domain Blacklist](img/domain_blacklist.png)
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 008872b59a7..699318e2479 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -237,23 +237,24 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
-| input | references |
-|:-----------------------|:--------------------------- |
-| `@user_name` | specific user |
-| `@group_name` | specific group |
-| `@all` | entire team |
-| `#123` | issue |
-| `!123` | merge request |
-| `$123` | snippet |
-| `~123` | label by ID |
-| `~bug` | one-word label by name |
-| `~"feature request"` | multi-word label by name |
-| `%123` | milestone by ID |
-| `%v1.23` | one-word milestone by name |
-| `%"release candidate"` | multi-word milestone by name |
-| `9ba12248` | specific commit |
-| `9ba12248...b19a04f5` | commit range comparison |
-| `[README](doc/README)` | repository file references |
+| input | references |
+|:---------------------------|:--------------------------------|
+| `@user_name` | specific user |
+| `@group_name` | specific group |
+| `@all` | entire team |
+| `#123` | issue |
+| `!123` | merge request |
+| `$123` | snippet |
+| `~123` | label by ID |
+| `~bug` | one-word label by name |
+| `~"feature request"` | multi-word label by name |
+| `%123` | milestone by ID |
+| `%v1.23` | one-word milestone by name |
+| `%"release candidate"` | multi-word milestone by name |
+| `9ba12248` | specific commit |
+| `9ba12248...b19a04f5` | commit range comparison |
+| `[README](doc/README)` | repository file references |
+| `[README](doc/README#L13)` | repository file line references |
GFM also recognizes certain cross-project references:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 678fc3ffd1f..e87cae092a5 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -62,11 +62,14 @@ The following table depicts the various user permission levels in a project.
| Manage runners | | | | ✓ | ✓ |
| Manage build triggers | | | | ✓ | ✓ |
| Manage variables | | | | ✓ | ✓ |
+| Manage pages | | | | ✓ | ✓ |
+| Manage pages domains and certificates | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Force push to protected branches [^3] | | | | | |
| Remove protected branches [^3] | | | | | |
+| Remove pages | | | | | ✓ |
## Group
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index cc688a7f99c..a23ad79ae1d 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -206,7 +206,7 @@ Sign in and re-enable two-factor authentication as soon as possible.
## Note to GitLab administrators
- You need to take special care to that 2FA keeps working after
-[restoring a GitLab backup](../raketasks/backup_restore.md).
+[restoring a GitLab backup](../../../raketasks/backup_restore.md).
- To ensure 2FA authorizes correctly with TOTP server, you may want to ensure
your GitLab server's time is synchronized via a service like NTP. Otherwise,
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 47a4a3f85d0..91b35c73b34 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -1,10 +1,7 @@
# GitLab Container Registry
-> [Introduced][ce-4040] in GitLab 8.8.
-
----
-
>**Notes:**
+> [Introduced][ce-4040] in GitLab 8.8.
- Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
versions earlier than 1.10.
- This document is about the user guide. To learn how to enable GitLab Container
@@ -98,8 +95,8 @@ delete them.
This feature requires GitLab 8.8 and GitLab Runner 1.2.
Make sure that your GitLab Runner is configured to allow building Docker images by
-following the [Using Docker Build](../ci/docker/using_docker_build.md)
-and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+following the [Using Docker Build](../../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
@@ -252,4 +249,4 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
index 95e8532e908..b636cb294b8 100644
--- a/doc/user/project/img/issue_board.png
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png
deleted file mode 100644
index fbb67b9c18f..00000000000
--- a/doc/user/project/img/issue_board_search_backlog.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
index 5bfdac88dde..5318e6ea4a9 100644
--- a/doc/user/project/img/issue_board_welcome_message.png
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_add_issues_modal.png b/doc/user/project/img/issue_boards_add_issues_modal.png
new file mode 100644
index 00000000000..33049dce74f
--- /dev/null
+++ b/doc/user/project/img/issue_boards_add_issues_modal.png
Binary files differ
diff --git a/doc/user/project/img/issue_boards_remove_issue.png b/doc/user/project/img/issue_boards_remove_issue.png
new file mode 100644
index 00000000000..8b3beca97cf
--- /dev/null
+++ b/doc/user/project/img/issue_boards_remove_issue.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png
index 1c05cb8fd36..320e6eb7fee 100644
--- a/doc/user/project/img/protected_branches_devs_can_push.png
+++ b/doc/user/project/img/protected_branches_devs_can_push.png
Binary files differ
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
new file mode 100644
index 00000000000..cad4757f287
--- /dev/null
+++ b/doc/user/project/integrations/bamboo.md
@@ -0,0 +1,59 @@
+# Atlassian Bamboo CI Service
+
+GitLab provides integration with Atlassian Bamboo for continuous integration.
+When configured, pushes to a project will trigger a build in Bamboo automatically.
+Merge requests will also display CI status showing whether the build is pending,
+failed, or completed successfully. It also provides a link to the Bamboo build
+page for more information.
+
+Bamboo doesn't quite provide the same features as a traditional build system when
+it comes to accepting webhooks and commit data. There are a few things that
+need to be configured in a Bamboo build plan before GitLab can integrate.
+
+## Setup
+
+### Complete these steps in Bamboo
+
+1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
+ dropdown.
+1. Select the 'Triggers' tab.
+1. Click 'Add trigger'.
+1. Enter a description such as 'GitLab trigger'
+1. Choose 'Repository triggers the build when changes are committed'
+1. Check one or more repositories checkboxes
+1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
+ whitelist of IP addresses that are allowed to trigger Bamboo builds.
+1. Save the trigger.
+1. In the left pane, select a build stage. If you have multiple build stages
+ you want to select the last stage that contains the git checkout task.
+1. Select the 'Miscellaneous' tab.
+1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
+ in the 'Labels' box.
+1. Save
+
+Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure to trigger builds.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click 'Atlassian Bamboo CI'
+1. Select the 'Active' checkbox.
+1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
+1. Enter the build key from your Bamboo build plan. Build keys are a short,
+ all capital letter, identifier that is unique. It will be something like PR-BLD
+1. If necessary, enter username and password for a Bamboo user that has
+ access to trigger the build plan. Leave these fields blank if you do not require
+ authentication.
+1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
+ will actually trigger a build in Bamboo.
+
+## Troubleshooting
+
+If builds are not triggered, these are a couple of things to keep in mind.
+
+1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
+ IP addresses'.
+1. Remember that GitLab only triggers builds on push events. A commit via the
+ web interface will not trigger CI currently.
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
new file mode 100644
index 00000000000..0b219e84478
--- /dev/null
+++ b/doc/user/project/integrations/bugzilla.md
@@ -0,0 +1,18 @@
+# Bugzilla Service
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services),
+select the **Bugzilla** service and fill in the required details as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
+
+Once you have configured and enabled Bugzilla:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Bugzilla product page
+- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
new file mode 100644
index 00000000000..f769dece242
--- /dev/null
+++ b/doc/user/project/integrations/builds_emails.md
@@ -0,0 +1,15 @@
+# Enabling build emails
+
+By enabling this service, you will be able to receive e-mail notifications about
+the result status of your builds.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Builds emails** service to configure it.
+
+In the _Recipients_ area, provide a list of e-mails separated by comma.
+
+Check the _Add pusher_ checkbox if you want the committer to also receive
+e-mail notifications about each build's status.
+
+If you enable the _Notify only broken builds_ option, e-mail notifications will
+be sent only for failed builds.
diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md
new file mode 100644
index 00000000000..18109fc049c
--- /dev/null
+++ b/doc/user/project/integrations/emails_on_push.md
@@ -0,0 +1,20 @@
+# Enabling emails on push
+
+By enabling this service, you will be able to receive email notifications for
+every change that is pushed to your project.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Emails on push** service to configure it.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md
new file mode 100644
index 00000000000..eee779c50d4
--- /dev/null
+++ b/doc/user/project/integrations/hipchat.md
@@ -0,0 +1,53 @@
+# Atlassian HipChat
+
+GitLab provides a way to send HipChat notifications upon a number of events,
+such as when a user pushes code, creates a branch or tag, adds a comment, and
+creates a merge request.
+
+## Setup
+
+GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
+not supported at this time. Note the differences between v1 and v2 tokens:
+
+HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
+token is allowed to send messages to *any* room.
+
+HipChat v2 API has tokens that are can be created using the Integrations tab
+in the Group or Room admin page. By design, these are lightweight tokens that
+allow GitLab to send messages only to *one* room.
+
+### Complete these steps in HipChat
+
+1. Go to: https://admin.hipchat.com/admin
+1. Click on "Group Admin" -> "Integrations".
+1. Find "Build Your Own!" and click "Create".
+1. Select the desired room, name the integration "GitLab", and click "Create".
+1. In the "Send messages to this room by posting this URL" column, you should
+see a URL in the format:
+
+```
+https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+```
+
+HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
+service in GitLab.
+
+### Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "HipChat".
+1. Select the "Active" checkbox.
+1. Insert the `token` field from the URL into the `Token` field on the Web page.
+1. Insert the `room` field from the URL into the `Room` field on the Web page.
+1. Save or optionally click "Test Settings".
+
+## Troubleshooting
+
+If you do not see notifications, make sure you are using a HipChat v2 API
+token, not a v1 token.
+
+Note that the v2 token is tied to a specific room. If you want to be able to
+specify arbitrary rooms, you can create an API token for a specific user in
+HipChat under "Account settings" and "API access". Use the `XXX` value under
+`auth_token=XXX`.
diff --git a/doc/user/project/integrations/img/accessing_integrations.png b/doc/user/project/integrations/img/accessing_integrations.png
new file mode 100644
index 00000000000..3b941f64998
--- /dev/null
+++ b/doc/user/project/integrations/img/accessing_integrations.png
Binary files differ
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png
index df301aa1eeb..df301aa1eeb 100644
--- a/doc/project_services/img/emails_on_push_service.png
+++ b/doc/user/project/integrations/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png
index 27dac49260c..27dac49260c 100644
--- a/doc/project_services/img/jira_add_user_to_group.png
+++ b/doc/user/project/integrations/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png
index 06c4e84fc61..06c4e84fc61 100644
--- a/doc/project_services/img/jira_create_new_group.png
+++ b/doc/user/project/integrations/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/user/project/integrations/img/jira_create_new_group_name.png
index bfc0dc6b2e9..bfc0dc6b2e9 100644
--- a/doc/project_services/img/jira_create_new_group_name.png
+++ b/doc/user/project/integrations/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png
index e9c03ed770d..e9c03ed770d 100644
--- a/doc/project_services/img/jira_create_new_user.png
+++ b/doc/user/project/integrations/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png
index 9d64cc57269..9d64cc57269 100644
--- a/doc/project_services/img/jira_group_access.png
+++ b/doc/user/project/integrations/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/user/project/integrations/img/jira_issue_reference.png
index 72c81460df7..72c81460df7 100644
--- a/doc/project_services/img/jira_issue_reference.png
+++ b/doc/user/project/integrations/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/user/project/integrations/img/jira_merge_request_close.png
index 0f82ceba557..0f82ceba557 100644
--- a/doc/project_services/img/jira_merge_request_close.png
+++ b/doc/user/project/integrations/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png
index 8540a427461..8540a427461 100644
--- a/doc/project_services/img/jira_project_name.png
+++ b/doc/user/project/integrations/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png
index 8e073b84ff9..8e073b84ff9 100644
--- a/doc/project_services/img/jira_service.png
+++ b/doc/user/project/integrations/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/user/project/integrations/img/jira_service_close_comment.png
index bb9cd7e3d13..bb9cd7e3d13 100644
--- a/doc/project_services/img/jira_service_close_comment.png
+++ b/doc/user/project/integrations/img/jira_service_close_comment.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/user/project/integrations/img/jira_service_close_issue.png
index c85b1d1dd97..c85b1d1dd97 100644
--- a/doc/project_services/img/jira_service_close_issue.png
+++ b/doc/user/project/integrations/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png
index c74351b57b8..c74351b57b8 100644
--- a/doc/project_services/img/jira_service_page.png
+++ b/doc/user/project/integrations/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png
index f81c5b5fc87..f81c5b5fc87 100644
--- a/doc/project_services/img/jira_user_management_link.png
+++ b/doc/user/project/integrations/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png
index e62fb202613..e62fb202613 100644
--- a/doc/project_services/img/jira_workflow_screenshot.png
+++ b/doc/user/project/integrations/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
index 349a2dc8456..349a2dc8456 100644
--- a/doc/project_services/img/kubernetes_configuration.png
+++ b/doc/user/project/integrations/img/kubernetes_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/user/project/integrations/img/mattermost_add_slash_command.png
index 7759efa183c..7759efa183c 100644
--- a/doc/project_services/img/mattermost_add_slash_command.png
+++ b/doc/user/project/integrations/img/mattermost_add_slash_command.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/user/project/integrations/img/mattermost_bot_auth.png
index 830b7849f3d..830b7849f3d 100644
--- a/doc/project_services/img/mattermost_bot_auth.png
+++ b/doc/user/project/integrations/img/mattermost_bot_auth.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
index b51798cf10d..b51798cf10d 100644
--- a/doc/project_services/img/mattermost_bot_available_commands.png
+++ b/doc/user/project/integrations/img/mattermost_bot_available_commands.png
Binary files differ
diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png
new file mode 100644
index 00000000000..dd3481bc1f6
--- /dev/null
+++ b/doc/user/project/integrations/img/mattermost_config_help.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317..3c5ff5ee317 100644
--- a/doc/project_services/img/mattermost_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/user/project/integrations/img/mattermost_console_integrations.png
index 92a30da5be0..92a30da5be0 100644
--- a/doc/project_services/img/mattermost_console_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_console_integrations.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/user/project/integrations/img/mattermost_gitlab_token.png
index 257018914d2..257018914d2 100644
--- a/doc/project_services/img/mattermost_gitlab_token.png
+++ b/doc/user/project/integrations/img/mattermost_gitlab_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/user/project/integrations/img/mattermost_goto_console.png
index 3354c2a24b4..3354c2a24b4 100644
--- a/doc/project_services/img/mattermost_goto_console.png
+++ b/doc/user/project/integrations/img/mattermost_goto_console.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
index 12766ab2b34..12766ab2b34 100644
--- a/doc/project_services/img/mattermost_slash_command_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/user/project/integrations/img/mattermost_slash_command_token.png
index c38f37c203c..c38f37c203c 100644
--- a/doc/project_services/img/mattermost_slash_command_token.png
+++ b/doc/user/project/integrations/img/mattermost_slash_command_token.png
Binary files differ
diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/user/project/integrations/img/mattermost_team_integrations.png
index 69d4a231e5a..69d4a231e5a 100644
--- a/doc/project_services/img/mattermost_team_integrations.png
+++ b/doc/user/project/integrations/img/mattermost_team_integrations.png
Binary files differ
diff --git a/doc/user/project/integrations/img/project_services.png b/doc/user/project/integrations/img/project_services.png
new file mode 100644
index 00000000000..25b6cd5690b
--- /dev/null
+++ b/doc/user/project/integrations/img/project_services.png
Binary files differ
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/user/project/integrations/img/redmine_configuration.png
index 7b6dd271401..7b6dd271401 100644
--- a/doc/project_services/img/redmine_configuration.png
+++ b/doc/user/project/integrations/img/redmine_configuration.png
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png
index 50d20510daf..50d20510daf 100644
--- a/doc/project_services/img/services_templates_redmine_example.png
+++ b/doc/user/project/integrations/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b..fc8e58e686b 100644
--- a/doc/project_services/img/slack_configuration.png
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png
new file mode 100644
index 00000000000..7928fb7d495
--- /dev/null
+++ b/doc/user/project/integrations/img/slack_setup.png
Binary files differ
diff --git a/doc/web_hooks/ssl.png b/doc/user/project/integrations/img/webhooks_ssl.png
index 21ddec4ebdf..21ddec4ebdf 100644
--- a/doc/web_hooks/ssl.png
+++ b/doc/user/project/integrations/img/webhooks_ssl.png
Binary files differ
diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md
new file mode 100644
index 00000000000..99093ebaed5
--- /dev/null
+++ b/doc/user/project/integrations/index.md
@@ -0,0 +1,26 @@
+# Project integrations
+
+You can find the available integrations under the **Integrations** page by
+navigating to the cog icon in the upper right corner of your project. You need
+to have at least [master permission][permissions] on the project.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Project services
+
+Project services allow you to integrate GitLab with other applications.
+They are a bit like plugins in that they allow a lot of freedom in
+adding functionality to GitLab.
+
+[Learn more about project services.](project_services.md)
+
+## Project webhooks
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+[Learn more about webhooks.](webhooks.md)
+
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md
new file mode 100644
index 00000000000..c63ea1316fe
--- /dev/null
+++ b/doc/user/project/integrations/irker.md
@@ -0,0 +1,50 @@
+# Irker IRC Gateway
+
+GitLab provides a way to push update messages to an Irker server. When
+configured, pushes to a project will trigger the service to send data directly
+to the Irker server.
+
+See the project homepage for further info: https://gitlab.com/esr/irker
+
+## Needed setup
+
+You will first need an Irker daemon. You can download the Irker code from its
+repository on https://gitlab.com/esr/irker:
+
+```
+git clone https://gitlab.com/esr/irker.git
+```
+
+Once you have downloaded the code, you can run the python script named `irkerd`.
+This script is the gateway script, it acts both as an IRC client, for sending
+messages to an IRC server obviously, and as a TCP server, for receiving messages
+from the GitLab service.
+
+If the Irker server runs on the same machine, you are done. If not, you will
+need to follow the firsts steps of the next section.
+
+## Complete these steps in GitLab
+
+1. Navigate to the project you want to configure for notifications.
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click "Irker".
+1. Select the "Active" checkbox.
+1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
+in the `Server host` field on the Web page
+1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the
+`Server port` field on the Web page.
+1. Optional: if `Default IRC URI` is set, it has to be in the format
+`irc[s]://domain.name` and will be prepend to each and every channel provided
+by the user which is not a full URI.
+1. Specify the recipients (e.g. #channel1, user1, etc.)
+1. Save or optionally click "Test Settings".
+
+## Note on Irker recipients
+
+Irker accepts channel names of the form `chan` and `#chan`, both for the
+`#chan` channel. If you want to send messages in query, you will need to add
+`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter
+case, `Aorimn` is treated as a nick and no more as a channel name.
+
+Irker can also join password-protected channels. Users need to append
+`?key=thesecretpassword` to the chan name.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
new file mode 100644
index 00000000000..4c64d1e0907
--- /dev/null
+++ b/doc/user/project/integrations/jira.md
@@ -0,0 +1,209 @@
+# 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.
+
+## Configuration
+
+In order to enable the JIRA service in GitLab, you need to first configure the
+project in JIRA and then enter the correct values in GitLab.
+
+### 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
+
+>**Notes:**
+- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or
+ higher is required.
+- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified
+ the configuration options you have to enter. If you are using an older version,
+ [follow this documentation][jira-repo-old-docs].
+
+To enable JIRA integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **JIRA** service, and 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. E.g., `https://jira.example.com`. |
+| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
+| `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)
+
+---
+
+## 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
+
+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, e.g., `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 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 PROJECT-1`
+- `Closes PROJECT-1`
+- `Fixes PROJECT-1`
+
+where `PROJECT-1` is the issue ID of the JIRA project.
+
+### JIRA issue closing example
+
+Let's consider the following example:
+
+1. For the project named `PROJECT` in JIRA, we implemented a new feature
+ and created a merge request in GitLab.
+1. This feature was requested in JIRA issue `PROJECT-7` and the merge request
+ in GitLab contains the improvement
+1. In the merge request description we use the issue closing trigger
+ `Closes PROJECT-7`.
+1. Once the merge request is merged, the JIRA issue will be automatically closed
+ with a comment and an associated link to the commit that resolved the issue.
+
+---
+
+In the following screenshot you can see what the link references to the JIRA
+issue look like.
+
+![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.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 closes JIRA issue](img/jira_service_close_issue.png)
+
+---
+
+![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png)
+
+## Troubleshooting
+
+If things don't work as expected that's usually because you have configured
+incorrectly the JIRA-GitLab integration.
+
+### 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.
+JIRA issue references and update comments will not work if the GitLab issue tracker is disabled.
+
+### GitLab is unable to close a ticket
+
+Make sure the `Transition ID` you set within the JIRA settings matches the one
+your project needs to close a ticket.
+
+[services-templates]: services_templates.md
+[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
new file mode 100644
index 00000000000..cc67e667472
--- /dev/null
+++ b/doc/user/project/integrations/kubernetes.md
@@ -0,0 +1,66 @@
+# GitLab Kubernetes / OpenShift integration
+
+GitLab can be configured to interact with Kubernetes, or other systems using the
+Kubernetes API (such as OpenShift).
+
+Each project can be configured to connect to a different Kubernetes cluster, see
+the [configuration](#configuration) section.
+
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see the [Services Templates](services_templates.md) document.
+
+## Configuration
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+of your project and select the **Kubernetes** service to configure it.
+
+![Kubernetes configuration settings](img/kubernetes_configuration.png)
+
+The Kubernetes service takes the following arguments:
+
+1. Kubernetes namespace
+1. API URL
+1. Service token
+1. Custom CA bundle
+
+The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
+exposes several APIs - we want the "base" URL that is common to all of them,
+e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+GitLab authenticates against Kubernetes using service tokens, which are
+scoped to a particular `namespace`. If you don't have a service token yet,
+you can follow the
+[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
+to create one. You can also view or create service tokens in the
+[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
+`Config -> Secrets`.
+
+Fill in the service token and namespace according to the values you just got.
+If the API is using a self-signed TLS certificate, you'll also need to include
+the `ca.crt` contents as the `Custom CA bundle`.
+
+## Deployment variables
+
+The Kubernetes service exposes following
+[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
+GitLab CI build environment:
+
+- `KUBE_URL` - equal to the API URL
+- `KUBE_TOKEN`
+- `KUBE_NAMESPACE`
+- `KUBE_CA_PEM` - only if a custom CA bundle was specified
+
+## Web terminals
+
+>**NOTE:**
+Added in GitLab 8.15. You must be the project owner or have `master` permissions
+to use terminals. Support is currently limited to the first container in the
+first pod of your environment.
+
+When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
+support to your environments. This is based on the `exec` functionality found in
+Docker and Kubernetes, so you get a new shell session within your existing
+containers. To use this integration, you should deploy to Kubernetes using
+the deployment variables above, ensuring any pods you create are labelled with
+`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
new file mode 100644
index 00000000000..09ba9994d3a
--- /dev/null
+++ b/doc/user/project/integrations/mattermost.md
@@ -0,0 +1,46 @@
+# Mattermost Notifications Service
+
+## On Mattermost
+
+To enable Mattermost integration you must create an incoming webhook integration:
+
+1. Sign in to your Mattermost instance
+1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
+1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
+
+There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
+it on https://mattermost.example/admin_console/integrations/custom.
+
+Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
+
+## On GitLab
+
+After you set up Mattermost, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Mattermost notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Mattermost channel you want to send that event message, with `#town-square`
+being the default. The hash sign is optional.
+
+At the end, fill in your Mattermost details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
+| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+
+![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md
new file mode 100644
index 00000000000..488f61c77a3
--- /dev/null
+++ b/doc/user/project/integrations/mattermost_slash_commands.md
@@ -0,0 +1,164 @@
+# Mattermost slash commands
+
+> Introduced in GitLab 8.14
+
+Mattermost commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in Mattermost.
+
+## Prerequisites
+
+Mattermost 3.4 and up is required.
+
+If you have the Omnibus GitLab package installed, Mattermost is already bundled
+in it. All you have to do is configure it. Read more in the
+[Omnibus GitLab Mattermost documentation][omnimmdocs].
+
+## Automated Configuration
+
+If Mattermost is installed on the same server as GitLab, the configuration process can be
+done for you by GitLab.
+
+Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
+
+## Manual Configuration
+
+The configuration consists of two parts. First you need to enable the slash
+commands in Mattermost and then enable the service in GitLab.
+
+### Step 1. Enable custom slash commands in Mattermost
+
+This step is only required when using a source install, omnibus installs will be
+preconfigured with the right settings.
+
+The first thing to do in Mattermost is to enable custom slash commands from
+the administrator console.
+
+1. Log in with an account that has admin privileges and navigate to the system
+ console.
+
+ ![Mattermost go to console](img/mattermost_goto_console.png)
+
+ ---
+
+1. Click **Custom integrations** and set **Enable Custom Slash Commands**,
+ **Enable custom integrations to override usernames**, and **Override
+ custom integrations to override profile picture icons** to true
+
+ ![Mattermost console](img/mattermost_console_integrations.png)
+
+ ---
+
+1. Click **Save** at the bottom to save the changes.
+
+### Step 2. Open the Mattermost slash commands service in GitLab
+
+1. Open a new tab for GitLab, go to your project's
+ [Integrations page](project_services.md#accessing-the-project-services)
+ and select the **Mattermost command** service to configure it.
+ A screen will appear with all the values you need to copy in Mattermost as
+ described in the next step. Leave the window open.
+
+ >**Note:**
+ GitLab will propose some values for the Mattermost settings. The only one
+ required to copy-paste as-is is the **Request URL**, all the others are just
+ suggestions.
+
+ ![Mattermost setup instructions](img/mattermost_config_help.png)
+
+ ---
+
+1. Proceed to the next step and create a slash command in Mattermost with the
+ above values.
+
+### Step 3. Create a new custom slash command in Mattermost
+
+Now that you have enabled custom slash commands in Mattermost and opened
+the Mattermost slash commands service in GitLab, it's time to copy these values
+in a new slash command.
+
+1. Back to Mattermost, under your team page settings, you should see the
+ **Integrations** option.
+
+ ![Mattermost team integrations](img/mattermost_team_integrations.png)
+
+ ---
+
+1. Go to the **Slash Commands** integration and add a new one by clicking the
+ **Add Slash Command** button.
+
+ ![Mattermost add command](img/mattermost_add_slash_command.png)
+
+ ---
+
+1. Fill in the options for the custom command as described in
+ [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab).
+
+ >**Note:**
+ If you plan on connecting multiple projects, pick a slash command trigger
+ word that relates to your projects such as `/gitlab-project-name` or even
+ just `/project-name`. Only use `/gitlab` if you will only connect a single
+ project to your Mattermost team.
+
+ ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png)
+
+1. After you setup all the values, copy the token (we will use it below) and
+ click **Done**.
+
+ ![Mattermost slash command token](img/mattermost_slash_command_token.png)
+
+### Step 4. Copy the Mattermost token into the Mattermost slash command service
+
+1. In GitLab, paste the Mattermost token you copied in the previous step and
+ check the **Active** checkbox.
+
+ ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png)
+
+1. Click **Save changes** for the changes to take effect.
+
+---
+
+You are now set to start using slash commands in Mattermost that talk to the
+GitLab project you configured.
+
+## Authorizing Mattermost to interact with GitLab
+
+The first time a user will interact with the newly created slash commands,
+Mattermost will trigger an authorization process.
+
+![Mattermost bot authorize](img/mattermost_bot_auth.png)
+
+This will connect your Mattermost user with your GitLab user. You can
+see all authorized chat accounts in your profile's page under **Chat**.
+
+When the authorization process is complete, you can start interacting with
+GitLab using the Mattermost commands.
+
+## Available slash commands
+
+The available slash commands are:
+
+| Command | Description | Example |
+| ------- | ----------- | ------- |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
+
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
+
+![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
+
+## Permissions
+
+The permissions to run the [available commands](#available-slash-commands) derive from
+the [permissions you have on the project](../../permissions.md#project).
+
+## Further reading
+
+- [Mattermost slash commands documentation][mmslashdocs]
+- [Omnibus GitLab Mattermost][omnimmdocs]
+
+[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
+[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html
+[ciyaml]: ../../../ci/yaml/README.md
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
new file mode 100644
index 00000000000..a3a163a4c6b
--- /dev/null
+++ b/doc/user/project/integrations/project_services.md
@@ -0,0 +1,76 @@
+# Project services
+
+Project services allow you to integrate GitLab with other applications. They
+are a bit like plugins in that they allow a lot of freedom in adding
+functionality to GitLab.
+
+## Accessing the project services
+
+You can find the available services under the **Integrations** page in your
+project's settings.
+
+1. Navigate to the cog icon in the upper right corner of your project. You need
+ to have at least [master permission][permissions] on the project.
+
+ ![Accessing the services](img/accessing_integrations.png)
+
+1. There are more than 20 services to integrate with. Click on the one that you
+ want to configure.
+
+ ![Project services list](img/project_services.png)
+
+Below, you will find a list of the currently supported ones accompanied with
+comprehensive documentation.
+
+## Services
+
+Click on the service links to see further configuration instructions and details.
+
+| Service | Description |
+| ------- | ----------- |
+| Asana | Asana - Teamwork without email |
+| Assembla | Project Management Software (Source Commits Endpoint) |
+| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
+| Buildkite | Continuous integration and deployments |
+| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
+| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
+| Campfire | Simple web-based real-time group chat |
+| Custom Issue Tracker | Custom issue tracker |
+| Drone CI | Continuous Integration platform built on Docker, written in Go |
+| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
+| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
+| Flowdock | Flowdock is a collaboration web app for technical teams |
+| 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](jira.md) | JIRA issue tracker |
+| JetBrains TeamCity CI | A continuous integration and build server |
+| [Kubernetes](kubernetes.md) | A containerized deployment service |
+| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
+| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| [Slack Notifications](slack.md) | Receive event notifications in Slack |
+| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
+| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
+| [Redmine](redmine.md) | Redmine issue tracker |
+
+## Services templates
+
+Services templates is a way to set some predefined values in the Service of
+your liking which will then be pre-filled on each project's Service.
+
+Read more about [Services templates in this document](services_templates.md).
+
+## Contributing to project services
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available, please see the
+[project_services source directory][projects-code].
+
+Contributions are welcome!
+
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
+[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
new file mode 100644
index 00000000000..89c0312d3c2
--- /dev/null
+++ b/doc/user/project/integrations/redmine.md
@@ -0,0 +1,23 @@
+# Redmine Service
+
+To enable the Redmine integration in a project, navigate to the
+[Integrations page](project_services.md#accessing-the-project-services), click
+the **Redmine** service, and fill in the required details on the page as described
+in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
+| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
+
+Once you have configured and enabled Redmine:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Redmine issue index
+- clicking **New issue** on the project dashboard creates a new Redmine issue
+
+As an example, below is a configuration for a project named gitlab-ci.
+
+![Redmine configuration](img/redmine_configuration.png)
diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md
new file mode 100644
index 00000000000..be6d13b6d2b
--- /dev/null
+++ b/doc/user/project/integrations/services_templates.md
@@ -0,0 +1,25 @@
+# Services Templates
+
+A GitLab administrator can add a service template that sets a default for each
+project. This makes it much easier to configure individual projects.
+
+After the template is created, the template details will be pre-filled on a
+project's Service page.
+
+## Enable a Service template
+
+In GitLab's Admin area, navigate to **Service Templates** and choose the
+service template you wish to create.
+
+For example, in the image below you can see Redmine.
+
+![Redmine service template](img/services_templates_redmine_example.png)
+
+---
+
+**NOTE:** For each project, you will still need to configure the issue tracking
+URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
+by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
+the project settings, and GitLab would automatically update the URL configured
+in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
+must be configured directly within the project's **Services** settings.
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
new file mode 100644
index 00000000000..57a9492044b
--- /dev/null
+++ b/doc/user/project/integrations/slack.md
@@ -0,0 +1,51 @@
+# Slack Notifications Service
+
+## On Slack
+
+To enable Slack integration you must create an incoming webhook integration on
+Slack:
+
+1. [Sign in to Slack](https://slack.com/signin)
+1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
+1. Choose the channel name you want to send notifications to.
+1. Click **Add Incoming WebHooks Integration**
+1. Copy the **Webhook URL**, we'll need this later for GitLab.
+
+## On GitLab
+
+After you set up Slack, it's time to set up GitLab.
+
+Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack notifications** service to configure it.
+There, you will see a checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Slack channel you want to send that event message, with `#general`
+being the default. Enter your preferred channel **without** the hash sign (`#`).
+
+At the end, fill in your Slack details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
+| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+After you are all done, click **Save changes** for the changes to take effect.
+
+>**Note:**
+You can set "branch,pushed,Compare changes" as highlight words on your Slack
+profile settings, so that you can be aware of new commits when somebody pushes
+them.
+
+![Slack configuration](img/slack_configuration.png)
+
+[slackhook]: https://my.slack.com/services/new/incoming-webhook
diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md
new file mode 100644
index 00000000000..56f1ba7311e
--- /dev/null
+++ b/doc/user/project/integrations/slack_slash_commands.md
@@ -0,0 +1,24 @@
+# Slack slash commands
+
+> Introduced in GitLab 8.15
+
+Slack commands give users an extra interface to perform common operations
+from the chat environment. This allows one to, for example, create an issue as
+soon as the idea was discussed in chat.
+For all available commands try the help subcommand, for example: `/gitlab help`,
+all review the [full list of commands](../../../integration/chat_commands.md).
+
+## Prerequisites
+
+A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
+Slack should be created beforehand, GitLab cannot create it for you.
+
+## Configuration
+
+Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
+and select the **Slack slash commands** service to configure it.
+
+![Slack setup instructions](img/slack_setup.png)
+
+Once you've followed the instructions, mark the service as active and insert the token
+you've received from Slack. After saving the service you are good to go!
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
new file mode 100644
index 00000000000..9df0c765f84
--- /dev/null
+++ b/doc/user/project/integrations/webhooks.md
@@ -0,0 +1,1028 @@
+# Webhooks
+
+>**Note:**
+Starting from GitLab 8.5:
+- the `repository` key is deprecated in favor of the `project` key
+- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
+- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
+
+Project webhooks allow you to trigger a URL if for example new code is pushed or
+a new issue is created. You can configure webhooks to listen for specific events
+like pushes, issues or merge requests. GitLab will send a POST request with data
+to the webhook URL.
+
+Webhooks can be used to update an external issue tracker, trigger CI builds,
+update a backup mirror, or even deploy to your production server.
+
+Navigate to the webhooks page by going to the **Integrations** page from your
+project's settings which can be found under the wheel icon in the upper right
+corner.
+
+![Accessing the integrations](img/accessing_integrations.png)
+
+## Webhook endpoint tips
+
+If you are writing your own endpoint (web server) that will receive
+GitLab webhooks keep in mind the following things:
+
+- Your endpoint should send its HTTP response as fast as possible. If
+ you wait too long, GitLab may decide the hook failed and retry it.
+- Your endpoint should ALWAYS return a valid HTTP response. If you do
+ not do this then GitLab will think the hook failed and retry it.
+ Most HTTP libraries take care of this for you automatically but if
+ you are writing a low-level hook this is important to remember.
+- GitLab ignores the HTTP status code returned by your endpoint.
+
+## Secret token
+
+If you specify a secret token, it will be sent with the hook request in the
+`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
+that the request is legitimate.
+
+## SSL verification
+
+By default, the SSL certificate of the webhook endpoint is verified based on
+an internal list of Certificate Authorities, which means the certificate cannot
+be self-signed.
+
+You can turn this off in the webhook settings in your GitLab projects.
+
+![SSL Verification](img/webhooks_ssl.png)
+
+## Events
+
+Below are described the supported events.
+
+### Push events
+
+Triggered when you push to the repository except when pushing tags.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "push",
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "user_id": 4,
+ "user_name": "John Smith",
+ "user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 15,
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "ssh_url":"git@example.com:mike/diaspora.git",
+ "http_url":"http://example.com/mike/diaspora.git"
+ },
+ "repository":{
+ "name": "Diaspora",
+ "url": "git@example.com:mike/diaspora.git",
+ "description": "",
+ "homepage": "http://example.com/mike/diaspora",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "visibility_level":0
+ },
+ "commits": [
+ {
+ "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "message": "Update Catalan translation to e38cb41.",
+ "timestamp": "2011-12-12T14:27:31+02:00",
+ "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "author": {
+ "name": "Jordi Mallach",
+ "email": "jordi@softcatala.org"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ },
+ {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
+ }
+ ],
+ "total_commits_count": 4
+}
+```
+
+### Tag events
+
+Triggered when you create (or delete) tags to the repository.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Tag Push Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "tag_push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
+ "name": "Example",
+ "url": "ssh://git@example.com/jsmith/example.git",
+ "description": "",
+ "homepage": "http://example.com/jsmith/example",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
+
+### Issues events
+
+Triggered when a new issue is created or an existing issue was updated/closed/reopened.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Issue Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "issue",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 301,
+ "title": "New API: create/update/delete file",
+ "assignee_id": 51,
+ "author_id": 51,
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "position": 0,
+ "branch_name": null,
+ "description": "Create new API for manipulations with repository",
+ "milestone_id": null,
+ "state": "opened",
+ "iid": 23,
+ "url": "http://example.com/diaspora/issues/23",
+ "action": "open"
+ },
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+}
+```
+### Comment events
+
+Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
+The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
+payload will also include information about the target of the comment. For example,
+a comment on a issue will include the specific issue information under the `issue` key.
+Valid target types:
+
+1. `commit`
+2. `merge_request`
+3. `issue`
+4. `snippet`
+
+#### Comment on commit
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlabhq/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
+ "namespace":"GitlabHQ",
+ "visibility_level":20,
+ "path_with_namespace":"gitlabhq/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlabhq/gitlab-test",
+ "url":"http://example.com/gitlabhq/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
+ "http_url":"http://example.com/gitlabhq/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1243,
+ "note": "This is a commit comment. How does this work?",
+ "noteable_type": "Commit",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:08:09 UTC",
+ "updated_at": "2015-05-17 18:08:09 UTC",
+ "project_id": 5,
+ "attachment":null,
+ "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
+ "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "noteable_id": null,
+ "system": false,
+ "st_diff": {
+ "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
+ "new_path": "six",
+ "old_path": "six",
+ "a_mode": "0",
+ "b_mode": "160000",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ },
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
+ },
+ "commit": {
+ "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "timestamp": "2014-02-27T10:06:20+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+}
+```
+
+#### Comment on merge request
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name": "Gitlab Test",
+ "url": "http://localhost/gitlab-org/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1244,
+ "note": "This MR needs work.",
+ "noteable_type": "MergeRequest",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:21:36 UTC",
+ "updated_at": "2015-05-17 18:21:36 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 7,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
+ },
+ "merge_request": {
+ "id": 7,
+ "target_branch": "markdown",
+ "source_branch": "master",
+ "source_project_id": 5,
+ "author_id": 8,
+ "assignee_id": 28,
+ "title": "Tempora et eos debitis quae laborum et.",
+ "created_at": "2015-03-01 20:12:53 UTC",
+ "updated_at": "2015-03-21 18:27:27 UTC",
+ "milestone_id": 11,
+ "state": "opened",
+ "merge_status": "cannot_be_merged",
+ "target_project_id": 5,
+ "iid": 1,
+ "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
+ "position": 0,
+ "locked_at": null,
+ "source":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "target": {
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "last_commit": {
+ "id": "562e173be03b8ff2efb05345d12df18815438a4b",
+ "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
+ "timestamp": "2015-04-08T21: 00:25-07:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
+ "author": {
+ "name": "John Smith",
+ "email": "john@example.com"
+ }
+ },
+ "work_in_progress": false,
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+#### Comment on issue
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "description":"",
+ "homepage":"http://example.com/mike/diaspora"
+ },
+ "object_attributes": {
+ "id": 1241,
+ "note": "Hello world",
+ "noteable_type": "Issue",
+ "author_id": 1,
+ "created_at": "2015-05-17 17:06:40 UTC",
+ "updated_at": "2015-05-17 17:06:40 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 92,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
+ },
+ "issue": {
+ "id": 92,
+ "title": "test",
+ "assignee_id": null,
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-12 14:53:17 UTC",
+ "updated_at": "2015-04-26 08:28:42 UTC",
+ "position": 0,
+ "branch_name": null,
+ "description": "test",
+ "milestone_id": null,
+ "state": "closed",
+ "iid": 17
+ }
+}
+```
+
+#### Comment on code snippet
+
+**Request header**:
+
+```
+X-Gitlab-Event: Note Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "note",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "project_id": 5,
+ "project":{
+ "name":"Gitlab Test",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/gitlab-org/gitlab-test",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
+ "namespace":"Gitlab Org",
+ "visibility_level":10,
+ "path_with_namespace":"gitlab-org/gitlab-test",
+ "default_branch":"master",
+ "homepage":"http://example.com/gitlab-org/gitlab-test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
+ "http_url":"http://example.com/gitlab-org/gitlab-test.git"
+ },
+ "repository":{
+ "name":"Gitlab Test",
+ "url":"http://example.com/gitlab-org/gitlab-test.git",
+ "description":"Aut reprehenderit ut est.",
+ "homepage":"http://example.com/gitlab-org/gitlab-test"
+ },
+ "object_attributes": {
+ "id": 1245,
+ "note": "Is this snippet doing what it's supposed to be doing?",
+ "noteable_type": "Snippet",
+ "author_id": 1,
+ "created_at": "2015-05-17 18:35:50 UTC",
+ "updated_at": "2015-05-17 18:35:50 UTC",
+ "project_id": 5,
+ "attachment": null,
+ "line_code": null,
+ "commit_id": "",
+ "noteable_id": 53,
+ "system": false,
+ "st_diff": null,
+ "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
+ },
+ "snippet": {
+ "id": 53,
+ "title": "test",
+ "content": "puts 'Hello world'",
+ "author_id": 1,
+ "project_id": 5,
+ "created_at": "2015-04-09 02:40:38 UTC",
+ "updated_at": "2015-04-09 02:40:38 UTC",
+ "file_name": "test.rb",
+ "expires_at": null,
+ "type": "ProjectSnippet",
+ "visibility_level": 0
+ }
+}
+```
+
+### Merge request events
+
+Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
+
+**Request header**:
+
+```
+X-Gitlab-Event: Merge Request Hook
+```
+
+**Request body:**
+
+```json
+{
+ "object_kind": "merge_request",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ },
+ "object_attributes": {
+ "id": 99,
+ "target_branch": "master",
+ "source_branch": "ms-viewport",
+ "source_project_id": 14,
+ "author_id": 51,
+ "assignee_id": 6,
+ "title": "MS-Viewport",
+ "created_at": "2013-12-03T17:23:34Z",
+ "updated_at": "2013-12-03T17:23:34Z",
+ "st_commits": null,
+ "st_diffs": null,
+ "milestone_id": null,
+ "state": "opened",
+ "merge_status": "unchecked",
+ "target_project_id": 14,
+ "iid": 1,
+ "description": "",
+ "source":{
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "target": {
+ "name":"Awesome Project",
+ "description":"Aut reprehenderit ut est.",
+ "web_url":"http://example.com/awesome_space/awesome_project",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "git_http_url":"http://example.com/awesome_space/awesome_project.git",
+ "namespace":"Awesome Space",
+ "visibility_level":20,
+ "path_with_namespace":"awesome_space/awesome_project",
+ "default_branch":"master",
+ "homepage":"http://example.com/awesome_space/awesome_project",
+ "url":"http://example.com/awesome_space/awesome_project.git",
+ "ssh_url":"git@example.com:awesome_space/awesome_project.git",
+ "http_url":"http://example.com/awesome_space/awesome_project.git"
+ },
+ "last_commit": {
+ "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "message": "fixed readme",
+ "timestamp": "2012-01-03T23:36:29+02:00",
+ "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "author": {
+ "name": "GitLab dev user",
+ "email": "gitlabdev@dv6700.(none)"
+ }
+ },
+ "work_in_progress": false,
+ "url": "http://example.com/diaspora/merge_requests/1",
+ "action": "open",
+ "assignee": {
+ "name": "User1",
+ "username": "user1",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
+ }
+ }
+}
+```
+
+### Wiki Page events
+
+Triggered when a wiki page is created or edited.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Wiki Page Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "wiki_page",
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
+ },
+ "project": {
+ "name": "awesome-project",
+ "description": "This is awesome",
+ "web_url": "http://example.com/root/awesome-project",
+ "avatar_url": null,
+ "git_ssh_url": "git@example.com:root/awesome-project.git",
+ "git_http_url": "http://example.com/root/awesome-project.git",
+ "namespace": "root",
+ "visibility_level": 0,
+ "path_with_namespace": "root/awesome-project",
+ "default_branch": "master",
+ "homepage": "http://example.com/root/awesome-project",
+ "url": "git@example.com:root/awesome-project.git",
+ "ssh_url": "git@example.com:root/awesome-project.git",
+ "http_url": "http://example.com/root/awesome-project.git"
+ },
+ "wiki": {
+ "web_url": "http://example.com/root/awesome-project/wikis/home",
+ "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
+ "git_http_url": "http://example.com/root/awesome-project.wiki.git",
+ "path_with_namespace": "root/awesome-project.wiki",
+ "default_branch": "master"
+ },
+ "object_attributes": {
+ "title": "Awesome",
+ "content": "awesome content goes here",
+ "format": "markdown",
+ "message": "adding an awesome page to the wiki",
+ "slug": "awesome",
+ "url": "http://example.com/root/awesome-project/wikis/awesome",
+ "action": "create"
+ }
+}
+```
+
+### Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "pipeline",
+ "object_attributes":{
+ "id": 31,
+ "ref": "master",
+ "tag": false,
+ "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "status": "success",
+ "stages":[
+ "build",
+ "test",
+ "deploy"
+ ],
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "duration": 63
+ },
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "project":{
+ "name": "Gitlab Test",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "avatar_url": null,
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "namespace": "Gitlab Org",
+ "visibility_level": 20,
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "default_branch": "master"
+ },
+ "commit":{
+ "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "message": "test\n",
+ "timestamp": "2016-08-12T17:23:21+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "author":{
+ "name": "User",
+ "email": "user@gitlab.com"
+ }
+ },
+ "builds":[
+ {
+ "id": 380,
+ "stage": "deploy",
+ "name": "production",
+ "status": "skipped",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "manual",
+ "manual": true,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 377,
+ "stage": "test",
+ "name": "test-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 378,
+ "stage": "test",
+ "name": "test-build",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 376,
+ "stage": "build",
+ "name": "build-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:24:56 UTC",
+ "finished_at": "2016-08-12 15:25:26 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 379,
+ "stage": "deploy",
+ "name": "staging",
+ "status": "created",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ }
+ ]
+}
+```
+
+### Build events
+
+Triggered on status change of a Build.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Build Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "build",
+ "ref": "gitlab-script-trigger",
+ "tag": false,
+ "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "build_id": 1977,
+ "build_name": "test",
+ "build_stage": "test",
+ "build_status": "created",
+ "build_started_at": null,
+ "build_finished_at": null,
+ "build_duration": null,
+ "build_allow_failure": false,
+ "project_id": 380,
+ "project_name": "gitlab-org/gitlab-test",
+ "user": {
+ "id": 3,
+ "name": "User",
+ "email": "user@gitlab.com"
+ },
+ "commit": {
+ "id": 2366,
+ "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
+ "message": "test\n",
+ "author_name": "User",
+ "author_email": "user@gitlab.com",
+ "status": "created",
+ "duration": null,
+ "started_at": null,
+ "finished_at": null
+ },
+ "repository": {
+ "name": "gitlab_test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "visibility_level": 20
+ }
+}
+```
+
+## Example webhook receiver
+
+If you want to see GitLab's webhooks in action for testing purposes you can use
+a simple echo script running in a console session. For the following script to
+work you need to have Ruby installed.
+
+Save the following file as `print_http_body.rb`:
+
+```ruby
+require 'webrick'
+
+server = WEBrick::HTTPServer.new(:Port => ARGV.first)
+server.mount_proc '/' do |req, res|
+ puts req.body
+end
+
+trap 'INT' do
+ server.shutdown
+end
+server.start
+```
+
+Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
+8000`. Then add your server as a webhook receiver in GitLab as
+`http://my.host:8000/`.
+
+When you press 'Test Hook' in GitLab, you should see something like this in the
+console:
+
+```
+{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
+example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
+- -> /
+```
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index d1ae57c00d7..3199d370a58 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -1,6 +1,8 @@
# Issue board
-> [Introduced][ce-5554] in GitLab 8.11.
+>**Notes:**
+- [Introduced][ce-5554] in GitLab 8.11.
+- The Backlog column was replaced by the **Add issues** button in GitLab 8.17.
The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board.
| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
-There are three types of lists, the ones you create based on your labels, and
-two default:
+There are two types of lists, the ones you create based on your labels, and
+one default:
-- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
-- **Done** (default): shows all closed issues. Always appears on the very right.
-Label list: a list based on a label. It shows all issues with that label.
- Label list: a list based on a label. It shows all opened issues with that label.
+- **Done** (default): shows all closed issues. Always appears on the very right.
![GitLab Issue Board](img/issue_board.png)
@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board:
If you are not able to perform one or more of the things above, make sure you
have the right [permissions](#permissions).
-## First time using the Issue Board
+## First time using the issue board
-The first time you navigate to your Issue Board, you will be presented with the
-two default lists (**Backlog** and **Done**) and a welcoming message that gives
+The first time you navigate to your Issue Board, you will be presented with
+a default list (**Done**) and a welcoming message that gives
you two options. You can either create a predefined set of labels and create
their corresponding lists to the Issue Board or opt-out and use your own lists.
@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm.
Deleting a list doesn't have any effect in issues and labels, it's just the
list view that is removed. You can always add it back later if you need.
-## Searching issues in the Backlog list
+## Adding issues to a list
+
+You can add issues to a list by clicking the **Add issues** button that is
+present in the upper right corner of the issue board. This will open up a modal
+window where you can see all the issues that do not belong to any list.
+
+Select one or more issues by clicking on the cards and then click **Add issues**
+to add them to the selected list. You can limit the issues you want to add to
+the list by filtering by author, assignee, milestone and label.
-The very first time you start using the Issue Board, it is very likely your
-issue tracker is already populated with labels and issues. In that case,
-**Backlog** will have all the issues that don't belong to another list, and
-**Done** will have all the closed ones.
+![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png)
-For performance and visibility reasons, each list shows the first 20 issues
-by default. If you have more than 20, you have to start scrolling down for the
-next 20 issues to appear. This can be cumbersome if your issue tracker hosts
-hundreds of issues, and for that reason it is easier to search for issues to
-move from **Backlog** to another list.
+## Removing an issue from a list
-Start typing in the search bar under the **Backlog** list and the relevant
-issues will appear.
+Removing an issue from a list can be done by clicking on the issue card and then
+clicking the **Remove from board** button in the sidebar. Under the hood, the
+respective label is removed, and as such it's also removed from the list and the
+board itself.
-![Issue Board search Backlog](img/issue_board_search_backlog.png)
+![Remove issue from list](img/issue_boards_remove_issue.png)
## Filtering issues
@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be:
and gets automatically closed.
For instance you can create a list based on the label of 'Frontend' and one for
-'Backend'. A designer can start working on an issue by dragging it from
-**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being
+'Backend'. A designer can start working on an issue by adding it to the
+'Frontend' list. That way, everyone knows that this issue is now being
worked on by the designers. Then, once they're done, all they have to do is
drag it over to the next list, 'Backend', where a backend developer can
eventually pick it up. Once they’re done, they move it to **Done**, to close the
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
index be09337319f..84a79f04094 100644
--- a/doc/user/project/merge_requests.md
+++ b/doc/user/project/merge_requests.md
@@ -1,169 +1 @@
-# Merge Requests
-
-Merge requests allow you to exchange changes you made to source code and
-collaborate with other people on the same project.
-
-## Authorization for merge requests
-
-There are two main ways to have a merge request flow with GitLab:
-
-1. Working with [protected branches][] in a single repository
-1. Working with forks of an authoritative project
-
-[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
-
-## Cherry-pick changes
-
-Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
-in a merged merge requests or a commit.
-
-[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
-
-## Merge when pipeline succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or
-more CI builds running, you can set it to be merged automatically when CI
-pipeline succeeds. This way, you don't have to wait for the pipeline to finish
-and remember to merge the request manually.
-
-[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md)
-
-## Resolve discussion comments in merge requests reviews
-
-Keep track of the progress during a code review with resolving comments.
-Resolving comments prevents you from forgetting to address feedback and lets
-you hide discussions that are no longer relevant.
-
-[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
-
-## Resolve conflicts
-
-When a merge request has conflicts, GitLab may provide the option to resolve
-those conflicts in the GitLab UI.
-
-[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
-
-## Revert changes
-
-GitLab implements Git's powerful feature to revert any commit with introducing
-a **Revert** button in merge requests and commit details.
-
-[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
-
-## Merge requests versions
-
-Every time you push to a branch that is tied to a merge request, a new version
-of merge request diff is created. When you visit a merge request that contains
-more than one pushes, you can select and compare the versions of those merge
-request diffs.
-
-[Read more about the merge requests versions.](merge_requests/versions.md)
-
-## Work In Progress merge requests
-
-To prevent merge requests from accidentally being accepted before they're
-completely ready, GitLab blocks the "Accept" button for merge requests that
-have been marked as a **Work In Progress**.
-
-[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
-
-## Ignore whitespace changes in Merge Request diff view
-
-If you click the **Hide whitespace changes** button, you can see the diff
-without whitespace changes (if there are any). This is also working when on a
-specific commit page.
-
-![MR diff](merge_requests/img/merge_request_diff.png)
-
->**Tip:**
-You can append `?w=1` while on the diffs page of a merge request to ignore any
-whitespace changes.
-
-## Tips
-
-Here are some tips that will help you be more efficient with merge requests in
-the command line.
-
-> **Note:**
-This section might move in its own document in the future.
-
-### Checkout merge requests locally
-
-A merge request contains all the history from a repository, plus the additional
-commits added to the branch associated with the merge request. Here's a few
-tricks to checkout a merge request locally.
-
-Please note that you can checkout a merge request locally even if the source
-project is a fork (even a private fork) of the target project.
-
-#### Checkout locally by adding a git alias
-
-Add the following alias to your `~/.gitconfig`:
-
-```
-[alias]
- mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
-```
-
-Now you can check out a particular merge request from any repository and any
-remote. For example, to check out the merge request with ID 5 as shown in GitLab
-from the `upstream` remote, do:
-
-```
-git mr upstream 5
-```
-
-This will fetch the merge request into a local `mr-upstream-5` branch and check
-it out.
-
-#### Checkout locally by modifying `.git/config` for a given repository
-
-Locate the section for your GitLab remote in the `.git/config` file. It looks
-like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-You can open the file with:
-
-```
-git config -e
-```
-
-Now add the following line to the above section:
-
-```
-fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-In the end, it should look like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
- fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests:
-
-```
-git fetch origin
-
-...
-From https://gitlab.com/gitlab-org/gitlab-ce.git
- * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-And to check out a particular merge request:
-
-```
-git checkout origin/merge-requests/1
-```
-
-[protected branches]: protected_branches.md
+This document was moved to [merge_requests/index.md](merge_requests/index.md).
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
new file mode 100644
index 00000000000..a27e683143d
--- /dev/null
+++ b/doc/user/project/merge_requests/index.md
@@ -0,0 +1,169 @@
+# Merge requests
+
+Merge requests allow you to exchange changes you made to source code and
+collaborate with other people on the same project.
+
+## Authorization for merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches][] in a single repository
+1. Working with forks of an authoritative project
+
+[Learn more about the authorization for merge requests.](authorization_for_merge_requests.md)
+
+## Cherry-pick changes
+
+Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
+in a merged merge requests or a commit.
+
+[Learn more about cherry-picking changes.](cherry_pick_changes.md)
+
+## Merge when pipeline succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when CI
+pipeline succeeds. This way, you don't have to wait for the pipeline to finish
+and remember to merge the request manually.
+
+[Learn more about merging when pipeline succeeds.](merge_when_pipeline_succeeds.md)
+
+## Resolve discussion comments in merge requests reviews
+
+Keep track of the progress during a code review with resolving comments.
+Resolving comments prevents you from forgetting to address feedback and lets
+you hide discussions that are no longer relevant.
+
+[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md)
+
+## Resolve conflicts
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI.
+
+[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
+
+## Revert changes
+
+GitLab implements Git's powerful feature to revert any commit with introducing
+a **Revert** button in merge requests and commit details.
+
+[Learn more about reverting changes in the UI](revert_changes.md)
+
+## Merge requests versions
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+[Read more about the merge requests versions.](versions.md)
+
+## Work In Progress merge requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked as a **Work In Progress**.
+
+[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
+
+## Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+Please note that you can checkout a merge request locally even if the source
+project is a fork (even a private fork) of the target project.
+
+#### Checkout locally by adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any
+remote. For example, to check out the merge request with ID 5 as shown in GitLab
+from the `upstream` remote, do:
+
+```
+git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check
+it out.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-ce.git
+ * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+[protected branches]: protected_branches.md
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 77eab7ba5e3..610250ccf12 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -1,6 +1,12 @@
# Merge requests versions
-> Will be [introduced][ce-5467] in GitLab 8.12.
+>**Notes:**
+- [Introduced][ce-5467] in GitLab 8.12.
+- Comments are disabled while viewing outdated merge versions or comparing to
+ versions other than base.
+- Merge request versions are based on push not on commit. So, if you pushed 5
+ commits in a single push, it will be a single option in the dropdown. If you
+ pushed 5 times, that will count for 5 options.
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
@@ -30,13 +36,4 @@ changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png)
----
-
->**Notes:**
-- Comments are disabled while viewing outdated merge versions or comparing to
- versions other than base.
-- Merge request versions are based on push not on commit. So, if you pushed 5
- commits in a single push, it will be a single option in the dropdown. If you
- pushed 5 times, that will count for 5 options.
-
[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
new file mode 100644
index 00000000000..a936d8e5dbd
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_project.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
new file mode 100644
index 00000000000..3f615d3757d
--- /dev/null
+++ b/doc/user/project/pages/img/pages_create_user_page.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
new file mode 100644
index 00000000000..8d34f3b7f38
--- /dev/null
+++ b/doc/user/project/pages/img/pages_dns_details.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
new file mode 100644
index 00000000000..2bc7cee07a6
--- /dev/null
+++ b/doc/user/project/pages/img/pages_multiple_domains.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
new file mode 100644
index 00000000000..c3640133bb2
--- /dev/null
+++ b/doc/user/project/pages/img/pages_new_domain_button.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png
new file mode 100644
index 00000000000..adbfb654877
--- /dev/null
+++ b/doc/user/project/pages/img/pages_remove.png
Binary files differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
new file mode 100644
index 00000000000..06d85ab1971
--- /dev/null
+++ b/doc/user/project/pages/img/pages_upload_cert.png
Binary files differ
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
new file mode 100644
index 00000000000..b814e3fccb2
--- /dev/null
+++ b/doc/user/project/pages/index.md
@@ -0,0 +1,435 @@
+# GitLab Pages
+
+> **Notes:**
+> - This feature was [introduced][ee-80] in GitLab EE 8.3.
+> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5.
+> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
+> - This document is about the user guide. To learn how to enable GitLab Pages
+> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md).
+
+With GitLab Pages you can host for free your static websites on GitLab.
+Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can
+deploy static pages for your individual projects, your user or your group.
+
+Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific
+information, if you are using GitLab.com to host your website.
+
+## Getting started with GitLab Pages
+
+> **Note:**
+> In the rest of this document we will assume that the general domain name that
+> is used for GitLab Pages is `example.io`.
+
+In general there are two types of pages one might create:
+
+- Pages per user (`username.example.io`) or per group (`groupname.example.io`)
+- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
+
+In GitLab, usernames and groupnames are unique and we often refer to them
+as namespaces. There can be only one namespace in a GitLab instance. Below you
+can see the connection between the type of GitLab Pages, what the project name
+that is created on GitLab looks like and the website URL it will be ultimately
+be served on.
+
+| Type of GitLab Pages | The name of the project created in GitLab | Website URL |
+| -------------------- | ------------ | ----------- |
+| User pages | `username.example.io` | `http(s)://username.example.io` |
+| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` |
+| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` |
+| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`|
+
+> **Warning:**
+> There are some known [limitations](#limitations) regarding namespaces served
+> under the general domain name and HTTPS. Make sure to read that section.
+
+### GitLab Pages requirements
+
+In brief, this is what you need to upload your website in GitLab Pages:
+
+1. Find out the general domain name that is used for GitLab Pages
+ (ask your administrator). This is very important, so you should first make
+ sure you get that right.
+1. Create a project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages]
+1. Set up a GitLab Runner to build your website
+
+> **Note:**
+> If [shared runners](../ci/runners/README.md) are enabled by your GitLab
+> administrator, you should be able to use them instead of bringing your own.
+
+### User or group Pages
+
+For user and group pages, the name of the project should be specific to the
+username or groupname and the general domain name that is used for GitLab Pages.
+Head over your GitLab instance that supports GitLab Pages and create a
+repository named `username.example.io`, where `username` is your username on
+GitLab. If the first part of the project name doesn't match exactly your
+username, it won’t work, so make sure to get it right.
+
+To create a group page, the steps are the same like when creating a website for
+users. Just make sure that you are creating the project within the group's
+namespace.
+
+![Create a user-based pages project](img/pages_create_user_page.png)
+
+---
+
+After you push some static content to your repository and GitLab Runner uploads
+the artifacts to GitLab CI, you will be able to access your website under
+`http(s)://username.example.io`. Keep reading to find out how.
+
+>**Note:**
+If your username/groupname contains a dot, for example `foo.bar`, you will not
+be able to use the wildcard domain HTTPS, read more at [limitations](#limitations).
+
+### Project Pages
+
+GitLab Pages for projects can be created by both user and group accounts.
+The steps to create a project page for a user or a group are identical:
+
+1. Create a new project
+1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory
+ of your repository with a specific job named [`pages`][pages].
+1. Set up a GitLab Runner to build your website
+
+A user's project will be served under `http(s)://username.example.io/projectname`
+whereas a group's project under `http(s)://groupname.example.io/projectname`.
+
+### Explore the contents of `.gitlab-ci.yml`
+
+The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
+gives you absolute control over the build process. You can actually watch your
+website being built live by following the CI build traces.
+
+> **Note:**
+> Before reading this section, make sure you familiarize yourself with GitLab CI
+> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by
+> following our [quick start guide](../ci/quick_start/README.md).
+
+To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the
+rules below:
+
+1. A special job named [`pages`][pages] must be defined
+1. Any static content which will be served by GitLab Pages must be placed under
+ a `public/` directory
+1. `artifacts` with a path to the `public/` directory must be defined
+
+In its simplest form, `.gitlab-ci.yml` looks like:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+```
+
+When the Runner reaches to build the `pages` job, it executes whatever is
+defined in the `script` parameter and if the build completes with a non-zero
+exit status, it then uploads the `public/` directory to GitLab Pages.
+
+The `public/` directory should contain all the static content of your website.
+Depending on how you plan to publish your website, the steps defined in the
+[`script` parameter](../ci/yaml/README.md#script) may differ.
+
+Be aware that Pages are by default branch/tag agnostic and their deployment
+relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the
+`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to whatever branch or tag, the Pages will be
+overwritten. In the example below, we limit the Pages to be deployed whenever
+a commit is pushed only on the `master` branch:
+
+```yaml
+pages:
+ script:
+ - my_commands
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab. And since all these parameters were all under a `pages`
+job, the contents of the `public` directory will be served by GitLab Pages.
+
+#### How `.gitlab-ci.yml` looks like when the static content is in your repository
+
+Supposedly your repository contained the following files:
+
+```
+├── index.html
+├── css
+│   └── main.css
+└── js
+ └── main.js
+```
+
+Then the `.gitlab-ci.yml` example below simply moves all files from the root
+directory of the project to the `public/` directory. The `.public` workaround
+is so `cp` doesn't also copy `public/` to itself in an infinite loop:
+
+```yaml
+pages:
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
+```
+
+#### How `.gitlab-ci.yml` looks like when using a static generator
+
+In general, GitLab Pages support any kind of [static site generator][staticgen],
+since `.gitlab-ci.yml` can be configured to run any possible command.
+
+In the root directory of your Git repository, place the source files of your
+favorite static generator. Then provide a `.gitlab-ci.yml` file which is
+specific to your static generator.
+
+The example below, uses [Jekyll] to build the static site:
+
+```yaml
+image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1
+
+pages: # the build job must be named pages
+ script:
+ - gem install jekyll # we install jekyll
+ - jekyll build -d public/ # we tell jekyll to build the site for us
+ artifacts:
+ paths:
+ - public # this is where the site will live and the Runner uploads it in GitLab
+ only:
+ - master # this script is only affecting the master branch
+```
+
+Here, we used the Docker executor and in the first line we specified the base
+image against which our builds will run.
+
+You have to make sure that the generated static files are ultimately placed
+under the `public` directory, that's why in the `script` section we run the
+`jekyll` command that builds the website and puts all content in the `public/`
+directory. Depending on the static generator of your choice, this command will
+differ. Search in the documentation of the static generator you will use if
+there is an option to explicitly set the output directory. If there is not
+such an option, you can always add one more line under `script` to rename the
+resulting directory in `public/`.
+
+We then tell the Runner to treat the `public/` directory as `artifacts` and
+upload it to GitLab.
+
+---
+
+See the [jekyll example project][pages-jekyll] to better understand how this
+works.
+
+For a list of Pages projects, see the [example projects](#example-projects) to
+get you started.
+
+#### How to set up GitLab Pages in a repository where there's also actual code
+
+Remember that GitLab Pages are by default branch/tag agnostic and their
+deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit
+the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except),
+whenever a new commit is pushed to a branch that will be used specifically for
+your pages.
+
+That way, you can have your project's code in the `master` branch and use an
+orphan branch (let's name it `pages`) that will host your static generator site.
+
+You can create a new empty branch like this:
+
+```bash
+git checkout --orphan pages
+```
+
+The first commit made on this new branch will have no parents and it will be
+the root of a new history totally disconnected from all the other branches and
+commits. Push the source files of your static generator in the `pages` branch.
+
+Below is a copy of `.gitlab-ci.yml` where the most significant line is the last
+one, specifying to execute everything in the `pages` branch:
+
+```
+image: ruby:2.1
+
+pages:
+ script:
+ - gem install jekyll
+ - jekyll build -d public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - pages
+```
+
+See an example that has different files in the [`master` branch][jekyll-master]
+and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+also includes `.gitlab-ci.yml`.
+
+[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
+[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
+
+## Next steps
+
+So you have successfully deployed your website, congratulations! Let's check
+what more you can do with GitLab Pages.
+
+### Example projects
+
+Below is a list of example projects for GitLab Pages with a plain HTML website
+or various static site generators. Contributions are very welcome.
+
+- [Plain HTML](https://gitlab.com/pages/plain-html)
+- [Jekyll](https://gitlab.com/pages/jekyll)
+- [Hugo](https://gitlab.com/pages/hugo)
+- [Middleman](https://gitlab.com/pages/middleman)
+- [Hexo](https://gitlab.com/pages/hexo)
+- [Brunch](https://gitlab.com/pages/brunch)
+- [Metalsmith](https://gitlab.com/pages/metalsmith)
+- [Harp](https://gitlab.com/pages/harp)
+
+Visit the GitLab Pages group for a full list of example projects:
+<https://gitlab.com/groups/pages>.
+
+### Add a custom domain to your Pages website
+
+If this setting is enabled by your GitLab administrator, you should be able to
+see the **New Domain** button when visiting your project's settings through the
+gear icon in the top right and then navigating to **Pages**.
+
+![New domain button](img/pages_new_domain_button.png)
+
+---
+
+You can add multiple domains pointing to your website hosted under GitLab.
+Once the domain is added, you can see it listed under the **Domains** section.
+
+![Pages multiple domains](img/pages_multiple_domains.png)
+
+---
+
+As a last step, you need to configure your DNS and add a CNAME pointing to your
+user/group page. Click on the **Details** button of a domain for further
+instructions.
+
+![Pages DNS details](img/pages_dns_details.png)
+
+---
+
+>**Note:**
+Currently there is support only for custom domains on per-project basis. That
+means that if you add a custom domain (`example.com`) for your user website
+(`username.example.io`), a project that is served under `username.example.io/foo`,
+will not be accessible under `example.com/foo`.
+
+### Secure your custom domain website with TLS
+
+When you add a new custom domain, you also have the chance to add a TLS
+certificate. If this setting is enabled by your GitLab administrator, you
+should be able to see the option to upload the public certificate and the
+private key when adding a new domain.
+
+![Pages upload cert](img/pages_upload_cert.png)
+
+### Custom error codes pages
+
+You can provide your own 403 and 404 error pages by creating the `403.html` and
+`404.html` files respectively in the root directory of the `public/` directory
+that will be included in the artifacts. Usually this is the root directory of
+your project, but that may differ depending on your static generator
+configuration.
+
+If the case of `404.html`, there are different scenarios. For example:
+
+- If you use project Pages (served under `/projectname/`) and try to access
+ `/projectname/non/exsiting_file`, GitLab Pages will try to serve first
+ `/projectname/404.html`, and then `/404.html`.
+- If you use user/group Pages (served under `/`) and try to access
+ `/non/existing_file` GitLab Pages will try to serve `/404.html`.
+- If you use a custom domain and try to access `/non/existing_file`, GitLab
+ Pages will try to serve only `/404.html`.
+
+### Remove the contents of your pages
+
+If you ever feel the need to purge your Pages content, you can do so by going
+to your project's settings through the gear icon in the top right, and then
+navigating to **Pages**. Hit the **Remove pages** button and your Pages website
+will be deleted. Simple as that.
+
+![Remove pages](img/pages_remove.png)
+
+## GitLab Pages on GitLab.com
+
+If you are using GitLab.com to host your website, then:
+
+- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`.
+- Custom domains and TLS support are enabled.
+- Shared runners are enabled by default, provided for free and can be used to
+ build your website. If you want you can still bring your own Runner.
+
+The rest of the guide still applies.
+
+## Limitations
+
+When using Pages under the general domain of a GitLab instance (`*.example.io`),
+you _cannot_ use HTTPS with sub-subdomains. That means that if your
+username/groupname contains a dot, for example `foo.bar`, the domain
+`https://foo.bar.example.io` will _not_ work. This is a limitation of the
+[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+don't redirect HTTP to HTTPS.
+
+[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
+
+## Redirects in GitLab Pages
+
+Since you cannot use any custom server configuration files, like `.htaccess` or
+any `.conf` file for that matter, if you want to redirect a web page to another
+location, you can use the [HTTP meta refresh tag][metarefresh].
+
+Some static site generators provide plugins for that functionality so that you
+don't have to create and edit HTML files manually. For example, Jekyll has the
+[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from).
+
+## Frequently Asked Questions
+
+### Can I download my generated pages?
+
+Sure. All you need to do is download the artifacts archive from the build page.
+
+### Can I use GitLab Pages if my project is private?
+
+Yes. GitLab Pages don't care whether you set your project's visibility level
+to private, internal or public.
+
+### Do I need to create a user/group website before creating a project website?
+
+No, you don't. You can create your project first and it will be accessed under
+`http(s)://namespace.example.io/projectname`.
+
+## Known issues
+
+For a list of known issues, visit GitLab's [public issue tracker].
+
+---
+
+[jekyll]: http://jekyllrb.com/
+[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80
+[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173
+[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages
+[gitlab ci]: https://about.gitlab.com/gitlab-ci
+[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[pages]: ../ci/yaml/README.md#pages
+[staticgen]: https://www.staticgen.com/
+[pages-jekyll]: https://gitlab.com/pages/jekyll
+[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh
+[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages
+[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 675e89e4247..c415d566a7c 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -170,6 +170,5 @@ you commit the changes you will be taken to a new merge request form.
![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-![New file button](basicsimages/file_button.png)
[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
+[issue closing pattern]: ../issues/automatic_issue_closing.md
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dfc762fe1d3..be042ddf623 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -14,6 +14,11 @@
> raketask.
> - The exports are stored in a temporary [shared directory][tmp] and are deleted
> every 24 hours by a specific worker.
+> - Group members will get exported as project members, as long as the user has
+> master or admin access to the group where the exported project lives. An admin
+> in the import side is required to map the users, based on email or username.
+> Otherwise, a supplementary comment is left to mention the original author and
+> the MRs, notes or issues will be owned by the importer.
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
@@ -22,7 +27,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| -------- | -------- |
-| 8.13.0 to current | 0.1.5 |
+| 8.17.0 to current | 0.1.6 |
+| 8.13.0 | 0.1.5 |
| 8.12.0 | 0.1.4 |
| 8.10.3 | 0.1.3 |
| 8.10.0 | 0.1.2 |
@@ -47,6 +53,9 @@ The following items will NOT be exported:
- Build traces and artifacts
- LFS objects
+- Container registry images
+- CI variables
+- Any encrypted tokens
## Exporting a project and its data
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index a6546cffce2..2fddd7c6503 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -32,5 +32,6 @@ do.
| `/wip` | Toggle the Work In Progress status |
| <code>/estimate &lt;1w 3d 2h 14m&gt;</code> | Set time estimate |
| `/remove_estimate` | Remove estimated time |
-| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or substract spent time |
+| <code>/spend &lt;1h 30m &#124; -1h 5m&gt;</code> | Add or subtract spent time |
| `/remove_time_spent` | Remove time spent |
+| `/target_branch <Branch Name>` | Set target branch for current merge request |
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 1659dd1f6cb..0ebe5eea173 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1,1025 +1 @@
-# Webhooks
-
->**Note:**
-Starting from GitLab 8.5:
-- the `repository` key is deprecated in favor of the `project` key
-- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key
-- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key
-
-Project webhooks allow you to trigger a URL if for example new code is pushed or
-a new issue is created. You can configure webhooks to listen for specific events
-like pushes, issues or merge requests. GitLab will send a POST request with data
-to the webhook URL.
-
-Webhooks can be used to update an external issue tracker, trigger CI builds,
-update a backup mirror, or even deploy to your production server.
-
-Navigate to the webhooks page by choosing **Webhooks** from your project's
-settings which can be found under the wheel icon in the upper right corner.
-
-## Webhook endpoint tips
-
-If you are writing your own endpoint (web server) that will receive
-GitLab webhooks keep in mind the following things:
-
-- Your endpoint should send its HTTP response as fast as possible. If
- you wait too long, GitLab may decide the hook failed and retry it.
-- Your endpoint should ALWAYS return a valid HTTP response. If you do
- not do this then GitLab will think the hook failed and retry it.
- Most HTTP libraries take care of this for you automatically but if
- you are writing a low-level hook this is important to remember.
-- GitLab ignores the HTTP status code returned by your endpoint.
-
-## Secret token
-
-If you specify a secret token, it will be sent with the hook request in the
-`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify
-that the request is legitimate.
-
-## SSL verification
-
-By default, the SSL certificate of the webhook endpoint is verified based on
-an internal list of Certificate Authorities, which means the certificate cannot
-be self-signed.
-
-You can turn this off in the webhook settings in your GitLab projects.
-
-![SSL Verification](ssl.png)
-
-## Events
-
-Below are described the supported events.
-
-### Push events
-
-Triggered when you push to the repository except when pushing tags.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "push",
- "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
- "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "ref": "refs/heads/master",
- "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "user_id": 4,
- "user_name": "John Smith",
- "user_email": "john@example.com",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 15,
- "project":{
- "name":"Diaspora",
- "description":"",
- "web_url":"http://example.com/mike/diaspora",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "namespace":"Mike",
- "visibility_level":0,
- "path_with_namespace":"mike/diaspora",
- "default_branch":"master",
- "homepage":"http://example.com/mike/diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "ssh_url":"git@example.com:mike/diaspora.git",
- "http_url":"http://example.com/mike/diaspora.git"
- },
- "repository":{
- "name": "Diaspora",
- "url": "git@example.com:mike/diaspora.git",
- "description": "",
- "homepage": "http://example.com/mike/diaspora",
- "git_http_url":"http://example.com/mike/diaspora.git",
- "git_ssh_url":"git@example.com:mike/diaspora.git",
- "visibility_level":0
- },
- "commits": [
- {
- "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "message": "Update Catalan translation to e38cb41.",
- "timestamp": "2011-12-12T14:27:31+02:00",
- "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "author": {
- "name": "Jordi Mallach",
- "email": "jordi@softcatala.org"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- },
- {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- },
- "added": ["CHANGELOG"],
- "modified": ["app/controller/application.rb"],
- "removed": []
- }
- ],
- "total_commits_count": 4
-}
-```
-
-### Tag events
-
-Triggered when you create (or delete) tags to the repository.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Tag Push Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "tag_push",
- "before": "0000000000000000000000000000000000000000",
- "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "ref": "refs/tags/v1.0.0",
- "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
- "user_id": 1,
- "user_name": "John Smith",
- "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
- "project_id": 1,
- "project":{
- "name":"Example",
- "description":"",
- "web_url":"http://example.com/jsmith/example",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "git_http_url":"http://example.com/jsmith/example.git",
- "namespace":"Jsmith",
- "visibility_level":0,
- "path_with_namespace":"jsmith/example",
- "default_branch":"master",
- "homepage":"http://example.com/jsmith/example",
- "url":"git@example.com:jsmith/example.git",
- "ssh_url":"git@example.com:jsmith/example.git",
- "http_url":"http://example.com/jsmith/example.git"
- },
- "repository":{
- "name": "Example",
- "url": "ssh://git@example.com/jsmith/example.git",
- "description": "",
- "homepage": "http://example.com/jsmith/example",
- "git_http_url":"http://example.com/jsmith/example.git",
- "git_ssh_url":"git@example.com:jsmith/example.git",
- "visibility_level":0
- },
- "commits": [],
- "total_commits_count": 0
-}
-```
-
-### Issues events
-
-Triggered when a new issue is created or an existing issue was updated/closed/reopened.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Issue Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "issue",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlabhq/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlabhq/gitlab-test"
- },
- "object_attributes": {
- "id": 301,
- "title": "New API: create/update/delete file",
- "assignee_id": 51,
- "author_id": 51,
- "project_id": 14,
- "created_at": "2013-12-03T17:15:43Z",
- "updated_at": "2013-12-03T17:15:43Z",
- "position": 0,
- "branch_name": null,
- "description": "Create new API for manipulations with repository",
- "milestone_id": null,
- "state": "opened",
- "iid": 23,
- "url": "http://example.com/diaspora/issues/23",
- "action": "open"
- },
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
-}
-```
-### Comment events
-
-Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
-The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
-payload will also include information about the target of the comment. For example,
-a comment on a issue will include the specific issue information under the `issue` key.
-Valid target types:
-
-1. `commit`
-2. `merge_request`
-3. `issue`
-4. `snippet`
-
-#### Comment on commit
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlabhq/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "git_http_url":"http://example.com/gitlabhq/gitlab-test.git",
- "namespace":"GitlabHQ",
- "visibility_level":20,
- "path_with_namespace":"gitlabhq/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlabhq/gitlab-test",
- "url":"http://example.com/gitlabhq/gitlab-test.git",
- "ssh_url":"git@example.com:gitlabhq/gitlab-test.git",
- "http_url":"http://example.com/gitlabhq/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://example.com/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1243,
- "note": "This is a commit comment. How does this work?",
- "noteable_type": "Commit",
- "author_id": 1,
- "created_at": "2015-05-17 18:08:09 UTC",
- "updated_at": "2015-05-17 18:08:09 UTC",
- "project_id": 5,
- "attachment":null,
- "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1",
- "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "noteable_id": null,
- "system": false,
- "st_diff": {
- "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n",
- "new_path": "six",
- "old_path": "six",
- "a_mode": "0",
- "b_mode": "160000",
- "new_file": true,
- "renamed_file": false,
- "deleted_file": false
- },
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243"
- },
- "commit": {
- "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
- "timestamp": "2014-02-27T10:06:20+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
-}
-```
-
-#### Comment on merge request
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name": "Gitlab Test",
- "url": "http://localhost/gitlab-org/gitlab-test.git",
- "description": "Aut reprehenderit ut est.",
- "homepage": "http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1244,
- "note": "This MR needs work.",
- "noteable_type": "MergeRequest",
- "author_id": 1,
- "created_at": "2015-05-17 18:21:36 UTC",
- "updated_at": "2015-05-17 18:21:36 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 7,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244"
- },
- "merge_request": {
- "id": 7,
- "target_branch": "markdown",
- "source_branch": "master",
- "source_project_id": 5,
- "author_id": 8,
- "assignee_id": 28,
- "title": "Tempora et eos debitis quae laborum et.",
- "created_at": "2015-03-01 20:12:53 UTC",
- "updated_at": "2015-03-21 18:27:27 UTC",
- "milestone_id": 11,
- "state": "opened",
- "merge_status": "cannot_be_merged",
- "target_project_id": 5,
- "iid": 1,
- "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
- "position": 0,
- "locked_at": null,
- "source":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "target": {
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "last_commit": {
- "id": "562e173be03b8ff2efb05345d12df18815438a4b",
- "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n",
- "timestamp": "2015-04-08T21: 00:25-07:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b",
- "author": {
- "name": "John Smith",
- "email": "john@example.com"
- }
- },
- "work_in_progress": false,
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-#### Comment on issue
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"diaspora",
- "url":"git@example.com:mike/diaspora.git",
- "description":"",
- "homepage":"http://example.com/mike/diaspora"
- },
- "object_attributes": {
- "id": 1241,
- "note": "Hello world",
- "noteable_type": "Issue",
- "author_id": 1,
- "created_at": "2015-05-17 17:06:40 UTC",
- "updated_at": "2015-05-17 17:06:40 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 92,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241"
- },
- "issue": {
- "id": 92,
- "title": "test",
- "assignee_id": null,
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-12 14:53:17 UTC",
- "updated_at": "2015-04-26 08:28:42 UTC",
- "position": 0,
- "branch_name": null,
- "description": "test",
- "milestone_id": null,
- "state": "closed",
- "iid": 17
- }
-}
-```
-
-#### Comment on code snippet
-
-**Request header**:
-
-```
-X-Gitlab-Event: Note Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "note",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "project_id": 5,
- "project":{
- "name":"Gitlab Test",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/gitlab-org/gitlab-test",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "git_http_url":"http://example.com/gitlab-org/gitlab-test.git",
- "namespace":"Gitlab Org",
- "visibility_level":10,
- "path_with_namespace":"gitlab-org/gitlab-test",
- "default_branch":"master",
- "homepage":"http://example.com/gitlab-org/gitlab-test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "ssh_url":"git@example.com:gitlab-org/gitlab-test.git",
- "http_url":"http://example.com/gitlab-org/gitlab-test.git"
- },
- "repository":{
- "name":"Gitlab Test",
- "url":"http://example.com/gitlab-org/gitlab-test.git",
- "description":"Aut reprehenderit ut est.",
- "homepage":"http://example.com/gitlab-org/gitlab-test"
- },
- "object_attributes": {
- "id": 1245,
- "note": "Is this snippet doing what it's supposed to be doing?",
- "noteable_type": "Snippet",
- "author_id": 1,
- "created_at": "2015-05-17 18:35:50 UTC",
- "updated_at": "2015-05-17 18:35:50 UTC",
- "project_id": 5,
- "attachment": null,
- "line_code": null,
- "commit_id": "",
- "noteable_id": 53,
- "system": false,
- "st_diff": null,
- "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245"
- },
- "snippet": {
- "id": 53,
- "title": "test",
- "content": "puts 'Hello world'",
- "author_id": 1,
- "project_id": 5,
- "created_at": "2015-04-09 02:40:38 UTC",
- "updated_at": "2015-04-09 02:40:38 UTC",
- "file_name": "test.rb",
- "expires_at": null,
- "type": "ProjectSnippet",
- "visibility_level": 0
- }
-}
-```
-
-### Merge request events
-
-Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
-
-**Request header**:
-
-```
-X-Gitlab-Event: Merge Request Hook
-```
-
-**Request body:**
-
-```json
-{
- "object_kind": "merge_request",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- },
- "object_attributes": {
- "id": 99,
- "target_branch": "master",
- "source_branch": "ms-viewport",
- "source_project_id": 14,
- "author_id": 51,
- "assignee_id": 6,
- "title": "MS-Viewport",
- "created_at": "2013-12-03T17:23:34Z",
- "updated_at": "2013-12-03T17:23:34Z",
- "st_commits": null,
- "st_diffs": null,
- "milestone_id": null,
- "state": "opened",
- "merge_status": "unchecked",
- "target_project_id": 14,
- "iid": 1,
- "description": "",
- "source":{
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "target": {
- "name":"Awesome Project",
- "description":"Aut reprehenderit ut est.",
- "web_url":"http://example.com/awesome_space/awesome_project",
- "avatar_url":null,
- "git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "git_http_url":"http://example.com/awesome_space/awesome_project.git",
- "namespace":"Awesome Space",
- "visibility_level":20,
- "path_with_namespace":"awesome_space/awesome_project",
- "default_branch":"master",
- "homepage":"http://example.com/awesome_space/awesome_project",
- "url":"http://example.com/awesome_space/awesome_project.git",
- "ssh_url":"git@example.com:awesome_space/awesome_project.git",
- "http_url":"http://example.com/awesome_space/awesome_project.git"
- },
- "last_commit": {
- "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "message": "fixed readme",
- "timestamp": "2012-01-03T23:36:29+02:00",
- "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "author": {
- "name": "GitLab dev user",
- "email": "gitlabdev@dv6700.(none)"
- }
- },
- "work_in_progress": false,
- "url": "http://example.com/diaspora/merge_requests/1",
- "action": "open",
- "assignee": {
- "name": "User1",
- "username": "user1",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
- }
-}
-```
-
-### Wiki Page events
-
-Triggered when a wiki page is created or edited.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Wiki Page Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "wiki_page",
- "user": {
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon"
- },
- "project": {
- "name": "awesome-project",
- "description": "This is awesome",
- "web_url": "http://example.com/root/awesome-project",
- "avatar_url": null,
- "git_ssh_url": "git@example.com:root/awesome-project.git",
- "git_http_url": "http://example.com/root/awesome-project.git",
- "namespace": "root",
- "visibility_level": 0,
- "path_with_namespace": "root/awesome-project",
- "default_branch": "master",
- "homepage": "http://example.com/root/awesome-project",
- "url": "git@example.com:root/awesome-project.git",
- "ssh_url": "git@example.com:root/awesome-project.git",
- "http_url": "http://example.com/root/awesome-project.git"
- },
- "wiki": {
- "web_url": "http://example.com/root/awesome-project/wikis/home",
- "git_ssh_url": "git@example.com:root/awesome-project.wiki.git",
- "git_http_url": "http://example.com/root/awesome-project.wiki.git",
- "path_with_namespace": "root/awesome-project.wiki",
- "default_branch": "master"
- },
- "object_attributes": {
- "title": "Awesome",
- "content": "awesome content goes here",
- "format": "markdown",
- "message": "adding an awesome page to the wiki",
- "slug": "awesome",
- "url": "http://example.com/root/awesome-project/wikis/awesome",
- "action": "create"
- }
-}
-```
-
-### Pipeline events
-
-Triggered on status change of Pipeline.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Pipeline Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "pipeline",
- "object_attributes":{
- "id": 31,
- "ref": "master",
- "tag": false,
- "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "status": "success",
- "stages":[
- "build",
- "test",
- "deploy"
- ],
- "created_at": "2016-08-12 15:23:28 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "duration": 63
- },
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "project":{
- "name": "Gitlab Test",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "avatar_url": null,
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "namespace": "Gitlab Org",
- "visibility_level": 20,
- "path_with_namespace": "gitlab-org/gitlab-test",
- "default_branch": "master"
- },
- "commit":{
- "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "message": "test\n",
- "timestamp": "2016-08-12T17:23:21+02:00",
- "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
- "author":{
- "name": "User",
- "email": "user@gitlab.com"
- }
- },
- "builds":[
- {
- "id": 380,
- "stage": "deploy",
- "name": "production",
- "status": "skipped",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "manual",
- "manual": true,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 377,
- "stage": "test",
- "name": "test-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 378,
- "stage": "test",
- "name": "test-build",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:26:12 UTC",
- "finished_at": "2016-08-12 15:26:29 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 376,
- "stage": "build",
- "name": "build-image",
- "status": "success",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": "2016-08-12 15:24:56 UTC",
- "finished_at": "2016-08-12 15:25:26 UTC",
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- },
- {
- "id": 379,
- "stage": "deploy",
- "name": "staging",
- "status": "created",
- "created_at": "2016-08-12 15:23:28 UTC",
- "started_at": null,
- "finished_at": null,
- "when": "on_success",
- "manual": false,
- "user":{
- "name": "Administrator",
- "username": "root",
- "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
- },
- "runner": null,
- "artifacts_file":{
- "filename": null,
- "size": null
- }
- }
- ]
-}
-```
-
-### Build events
-
-Triggered on status change of a Build.
-
-**Request Header**:
-
-```
-X-Gitlab-Event: Build Hook
-```
-
-**Request Body**:
-
-```json
-{
- "object_kind": "build",
- "ref": "gitlab-script-trigger",
- "tag": false,
- "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "build_id": 1977,
- "build_name": "test",
- "build_stage": "test",
- "build_status": "created",
- "build_started_at": null,
- "build_finished_at": null,
- "build_duration": null,
- "build_allow_failure": false,
- "project_id": 380,
- "project_name": "gitlab-org/gitlab-test",
- "user": {
- "id": 3,
- "name": "User",
- "email": "user@gitlab.com"
- },
- "commit": {
- "id": 2366,
- "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f",
- "message": "test\n",
- "author_name": "User",
- "author_email": "user@gitlab.com",
- "status": "created",
- "duration": null,
- "started_at": null,
- "finished_at": null
- },
- "repository": {
- "name": "gitlab_test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "description": "Atque in sunt eos similique dolores voluptatem.",
- "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
- "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
- "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
- "visibility_level": 20
- }
-}
-```
-
-## Example webhook receiver
-
-If you want to see GitLab's webhooks in action for testing purposes you can use
-a simple echo script running in a console session. For the following script to
-work you need to have Ruby installed.
-
-Save the following file as `print_http_body.rb`:
-
-```ruby
-require 'webrick'
-
-server = WEBrick::HTTPServer.new(:Port => ARGV.first)
-server.mount_proc '/' do |req, res|
- puts req.body
-end
-
-trap 'INT' do
- server.shutdown
-end
-server.start
-```
-
-Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb
-8000`. Then add your server as a webhook receiver in GitLab as
-`http://my.host:8000/`.
-
-When you press 'Test Hook' in GitLab, you should see something like this in the
-console:
-
-```
-{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
-example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
-- -> /
-```
+This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md).
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 0b6f00c6aa4..7a97b87f1c5 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -27,7 +27,7 @@
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
-- [Merge Requests](../user/project/merge_requests.md)
+- [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 97380bce172..f3c636ed1d5 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -28,7 +28,7 @@ to enable this if not already.
When issues/pull requests are being imported, the Bitbucket importer tries to find
the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and [**associated their Bitbucket account**][social sign-in]. If the user is not
+and **associated their Bitbucket account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original Bitbucket author is kept.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 86a016fc6d6..cdacef9832f 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -28,7 +28,7 @@ still be able to import their GitHub repositories with a
When issues/pull requests are being imported, the GitHub importer tries to find
the GitHub author/assignee in GitLab's database using the GitHub ID. For this
to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and [**associated their GitHub account**][social sign-in]. If the user is not
+and **associated their GitHub account**. If the user is not
found in GitLab's database, the project creator (most of the times the current
user that started the import process) is set as the author, but a reference on
the issue about the original GitHub author is kept.
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 5f6a718135d..3a6773909d6 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -43,8 +43,8 @@ In `config/gitlab.yml`:
## Storage statistics
You can see the total storage used for LFS objects on groups and projects
-in the administration area, as well as through the [groups](../api/groups.md)
-and [projects APIs](../api/projects.md).
+in the administration area, as well as through the [groups](../../api/groups.md)
+and [projects APIs](../../api/projects.md).
## Known limitations
diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature
deleted file mode 100644
index 41d79aa6ec8..00000000000
--- a/features/dashboard/shortcuts.feature
+++ /dev/null
@@ -1,21 +0,0 @@
-@dashboard
-Feature: Dashboard Shortcuts
- Background:
- Given I sign in as a user
- And I visit dashboard page
-
- @javascript
- Scenario: Navigate to projects tab
- Given I press "g" and "p"
- Then the active main tab should be Projects
-
- @javascript
- Scenario: Navigate to issue tab
- Given I press "g" and "i"
- Then the active main tab should be Issues
-
- @javascript
- Scenario: Navigate to merge requests tab
- Given I press "g" and "m"
- Then the active main tab should be Merge Requests
-
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index d033e6b167b..5c14c5db665 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -53,6 +53,13 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
+ Scenario: On Project Settings/Pages
+ Given I visit my project's settings page
+ And I click the "Pages" tab
+ Then the active sub nav should be Pages
+ And no other sub navs should be active
+ And the active main tab should be Settings
+
Scenario: On Project Members
Given I visit my project's members page
Then the active sub nav should be Members
diff --git a/features/project/pages.feature b/features/project/pages.feature
new file mode 100644
index 00000000000..87d88348d09
--- /dev/null
+++ b/features/project/pages.feature
@@ -0,0 +1,82 @@
+Feature: Project Pages
+ Background:
+ Given I sign in as a user
+ And I own a project
+
+ Scenario: Pages are disabled
+ Given pages are disabled
+ When I visit the Project Pages
+ Then I should see that GitLab Pages are disabled
+
+ Scenario: I can see the pages usage if not deployed
+ Given pages are enabled
+ When I visit the Project Pages
+ Then I should see the usage of GitLab Pages
+
+ Scenario: I can access the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ Then I should be able to access the Pages
+
+ Scenario: I should message that domains support is disabled
+ Given pages are enabled
+ And pages are deployed
+ And support for external domains is disabled
+ When I visit the Project Pages
+ Then I should see that support for domains is disabled
+
+ Scenario: I should see a new domain button
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit the Project Pages
+ And I should be able to add a New Domain
+
+ Scenario: I should be able to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be able to add a new domain for project in group namespace
+ Given I own a project in some group namespace
+ And pages are enabled
+ And pages are exposed on external HTTP address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I should be denied to add the same domain twice
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I click on "Create New Domain"
+ Then I should see error message that domain already exists
+
+ Scenario: I should message that certificates support is disabled when trying to add a new domain
+ Given pages are enabled
+ And pages are exposed on external HTTP address
+ And pages domain is added
+ When I visit add a new Pages Domain
+ Then I should see that support for certificates is disabled
+
+ Scenario: I should be able to add a new domain with certificate
+ Given pages are enabled
+ And pages are exposed on external HTTPS address
+ When I visit add a new Pages Domain
+ And I fill the domain
+ And I fill the certificate and key
+ And I click on "Create New Domain"
+ Then I should see a new domain added
+
+ Scenario: I can remove the pages if deployed
+ Given pages are enabled
+ And pages are deployed
+ When I visit the Project Pages
+ And I click Remove Pages
+ Then The Pages should get removed
diff --git a/features/snippets/user.feature b/features/snippets/user.feature
deleted file mode 100644
index 5b5dadb7b39..00000000000
--- a/features/snippets/user.feature
+++ /dev/null
@@ -1,34 +0,0 @@
-@snippets
-Feature: Snippets User
- 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 all my snippets
- Given I visit my snippets page
- Then I should see "Personal snippet one" in snippets
- And I should see "Personal snippet private" in snippets
- And I should see "Personal snippet internal" in snippets
-
- Scenario: I can see only my private snippets
- Given I visit my snippets page
- And I click "Private" filter
- Then I should not see "Personal snippet one" in snippets
- And I should not see "Personal snippet internal" in snippets
- And I should see "Personal snippet private" in snippets
-
- Scenario: I can see only my public snippets
- Given I visit my snippets page
- And I click "Public" filter
- Then I should see "Personal snippet one" in snippets
- And I should not see "Personal snippet private" in snippets
- And I should not see "Personal snippet internal" in snippets
-
- Scenario: I can see only my internal snippets
- Given I visit my snippets page
- And I click "Internal" filter
- Then I should see "Personal snippet internal" in snippets
- And I should not see "Personal snippet private" in snippets
- And I should not see "Personal snippet one" in snippets
diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb
deleted file mode 100644
index 118d27888df..00000000000
--- a/features/steps/dashboard/shortcuts.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
- include SharedSidebarActiveTab
- include SharedShortcuts
-end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 344b6fda9a6..2bbc43b491f 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -25,15 +25,18 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should see todos assigned to me' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
page.within('.todos-pending-count') { expect(page).to have_content '4' }
expect(page).to have_content 'To do 4'
expect(page).to have_content 'Done 0'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title)
- should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?")
- should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title)
- should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?")
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title)
end
step 'I mark the todo as done' do
@@ -44,10 +47,13 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.todos-pending-count') { expect(page).to have_content '3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
end
step 'I mark all todos as done' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
click_link 'Mark all as done'
page.within('.todos-pending-count') { expect(page).to have_content '0' }
@@ -55,27 +61,30 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
expect(page).to have_content 'Done 4'
expect(page).to have_content "You're all done!"
expect('.prepend-top-default').not_to have_link project.name_with_namespace
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
- should_not_see_todo "John Doe mentioned you on issue #{issue.to_reference}"
- should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
- should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request_reference}"
+ should_not_see_todo "John Doe mentioned you on issue #{issue_reference}"
+ should_not_see_todo "John Doe assigned you issue #{issue_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue_reference}"
end
step 'I should see the todo marked as done' do
click_link 'Done 1'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, false)
end
step 'I should see all todos marked as done' do
+ merge_request_reference = merge_request.to_reference(full: true)
+ issue_reference = issue.to_reference(full: true)
+
click_link 'Done 4'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
- should_see_todo(2, "John Doe mentioned you on issue #{issue.to_reference}", "#{current_user.to_reference} Wdyt?", false)
- should_see_todo(3, "John Doe assigned you issue #{issue.to_reference}", issue.title, false)
- should_see_todo(4, "Mary Jane mentioned you on issue #{issue.to_reference}", issue.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request_reference}", merge_request.title, false)
+ should_see_todo(2, "John Doe mentioned you on issue #{issue_reference}", "#{current_user.to_reference} Wdyt?", false)
+ should_see_todo(3, "John Doe assigned you issue #{issue_reference}", issue.title, false)
+ should_see_todo(4, "Mary Jane mentioned you on issue #{issue_reference}", issue.title, false)
end
step 'I filter by "Enterprise"' do
@@ -111,16 +120,16 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should not see todos related to "Mary Jane" in the list' do
- should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference}"
+ should_not_see_todo "Mary Jane mentioned you on issue #{issue.to_reference(full: true)}"
end
step 'I should not see todos related to "Merge Requests" in the list' do
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
end
step 'I should not see todos related to "Assignments" in the list' do
- should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
- should_not_see_todo "John Doe assigned you issue #{issue.to_reference}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference(full: true)}"
+ should_not_see_todo "John Doe assigned you issue #{issue.to_reference(full: true)}"
end
step 'I click on the todo' do
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 9f701840f1d..e842d7bec2b 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -35,6 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
click_link('Deploy Keys')
end
+ step 'I click the "Pages" tab' do
+ click_link('Pages')
+ end
+
step 'the active sub nav should be Members' do
ensure_active_sub_nav('Members')
end
@@ -47,6 +51,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
ensure_active_sub_nav('Deploy Keys')
end
+ step 'the active sub nav should be Pages' do
+ ensure_active_sub_nav('Pages')
+ end
+
# Sub Tabs: Commits
step 'I click the "Compare" tab' do
diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb
index 374eb0b0e07..19ff92f6dc6 100644
--- a/features/steps/project/builds/summary.rb
+++ b/features/steps/project/builds/summary.rb
@@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps
step 'recent build summary contains information saying that build has been erased' do
page.within('.erased') do
- expect(page).to have_content 'Build has been erased'
+ expect(page).to have_content 'Job has been erased'
end
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 7490d2bc6e7..48ac7a98f0d 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -34,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Builds for last week'
- expect(page).to have_content 'Builds for last month'
- expect(page).to have_content 'Builds for last year'
+ expect(page).to have_content 'Jobs for last week'
+ expect(page).to have_content 'Jobs for last month'
+ expect(page).to have_content 'Jobs for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index f74a9b5df47..4a35b71af2f 100644
--- a/features/steps/project/issues/labels.rb
+++ b/features/steps/project/issues/labels.rb
@@ -15,17 +15,16 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do
page.within '.labels' do
- page.all('.remove-row').each do |remove|
- remove.click
- sleep 0.05
+ page.all('.remove-row').each do
+ first('.remove-row').click
end
end
end
step 'I should see labels help message' do
page.within '.labels' do
- expect(page).to have_content 'Create a label or generate a default set '\
- 'of labels'
+ expect(page).to have_content 'Generate a default set of labels'
+ expect(page).to have_content 'New label'
end
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index d2fa8cd39af..9f0057cace7 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -501,6 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I fill in merge request search with "Fe"' do
fill_in 'issuable_search', with: "Fe"
+ page.within '.merge-requests-holder' do
+ find('.merge-request')
+ end
end
step 'I click the "Target branch" dropdown' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
new file mode 100644
index 00000000000..c80c6273807
--- /dev/null
+++ b/features/steps/project/pages.rb
@@ -0,0 +1,139 @@
+class Spinach::Features::ProjectPages < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+
+ step 'pages are enabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.pages).to receive(:host).and_return('example.com')
+ allow(Gitlab.config.pages).to receive(:port).and_return(80)
+ allow(Gitlab.config.pages).to receive(:https).and_return(false)
+ end
+
+ step 'pages are disabled' do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(false)
+ end
+
+ step 'I visit the Project Pages' do
+ visit namespace_project_pages_path(@project.namespace, @project)
+ end
+
+ step 'I should see that GitLab Pages are disabled' do
+ expect(page).to have_content('GitLab Pages are disabled')
+ end
+
+ step 'I should see the usage of GitLab Pages' do
+ expect(page).to have_content('Configure pages')
+ end
+
+ step 'pages are deployed' do
+ pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha)
+ build = build(:ci_build,
+ project: @project,
+ pipeline: pipeline,
+ ref: 'HEAD',
+ artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'),
+ artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
+ )
+ result = ::Projects::UpdatePagesService.new(@project, build).execute
+ expect(result[:status]).to eq(:success)
+ end
+
+ step 'I should be able to access the Pages' do
+ expect(page).to have_content('Access pages')
+ end
+
+ step 'I should see that support for domains is disabled' do
+ expect(page).to have_content('Support for domains and certificates is disabled')
+ end
+
+ step 'support for external domains is disabled' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTP address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
+ end
+
+ step 'pages are exposed on external HTTPS address' do
+ allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+ end
+
+ step 'I should be able to add a New Domain' do
+ expect(page).to have_content('New Domain')
+ end
+
+ step 'I visit add a new Pages Domain' do
+ visit new_namespace_project_pages_domain_path(@project.namespace, @project)
+ end
+
+ step 'I fill the domain' do
+ fill_in 'Domain', with: 'my.test.domain.com'
+ end
+
+ step 'I click on "Create New Domain"' do
+ click_button 'Create New Domain'
+ end
+
+ step 'I should see a new domain added' do
+ expect(page).to have_content('Domains (1)')
+ expect(page).to have_content('my.test.domain.com')
+ end
+
+ step 'pages domain is added' do
+ @project.pages_domains.create!(domain: 'my.test.domain.com')
+ end
+
+ step 'I should see error message that domain already exists' do
+ expect(page).to have_content('Domain has already been taken')
+ end
+
+ step 'I should see that support for certificates is disabled' do
+ expect(page).to have_content('Support for custom certificates is disabled')
+ end
+
+ step 'I fill the certificate and key' do
+ fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+
+ fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ step 'I click Remove Pages' do
+ click_link 'Remove pages'
+ end
+
+ step 'The Pages should get removed' do
+ expect(@project.pages_deployed?).to be_falsey
+ end
+end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 70e6d4836b2..d008a8a26af 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -47,7 +47,7 @@ module SharedBuilds
end
step 'recent build has a build trace' do
- @build.trace = 'build trace'
+ @build.trace = 'job trace'
end
step 'download of build artifacts archive starts' do
@@ -60,7 +60,7 @@ module SharedBuilds
end
step 'I see details of a build' do
- expect(page).to have_content "Build ##{@build.id}"
+ expect(page).to have_content "Job ##{@build.id}"
end
step 'I see build trace' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 7a6707a7dfb..dae248b8b7e 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -7,6 +7,12 @@ module SharedProject
@project.team << [@user, :master]
end
+ step "I own a project in some group namespace" do
+ @group = create(:group, name: 'some group')
+ @project = create(:project, namespace: @group)
+ @project.team << [@user, :master]
+ end
+
step "project exists in some group namespace" do
@group = create(:group, name: 'some group')
@project = create(:project, :repository, namespace: @group, public_builds: false)
diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
deleted file mode 100644
index 997c605bce2..00000000000
--- a/features/steps/snippets/user.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedSnippet
-
- step 'I visit my snippets page' do
- visit dashboard_snippets_path
- end
-
- 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 private" in snippets' do
- expect(page).to have_content "Personal snippet private"
- 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 one" in snippets' do
- expect(page).not_to have_content "Personal snippet one"
- end
-
- step 'I should not see "Personal snippet private" in snippets' do
- expect(page).not_to have_content "Personal snippet private"
- end
-
- step 'I should not see "Personal snippet internal" in snippets' do
- expect(page).not_to have_content "Personal snippet internal"
- end
-
- step 'I click "Internal" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Internal"
- end
- end
-
- step 'I click "Private" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Private"
- end
- end
-
- step 'I click "Public" filter' do
- page.within('.snippet-scope-menu') do
- click_link "Public"
- end
- end
-
- def snippet
- @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one")
- end
-end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6cf6b501021..7ec089b9c29 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -1,7 +1,17 @@
module API
class API < Grape::API
include APIGuard
- version 'v3', using: :path
+
+ version %w(v3 v4), using: :path
+
+ version 'v3', using: :path do
+ mount ::API::V3::DeployKeys
+ mount ::API::V3::Issues
+ mount ::API::V3::Members
+ mount ::API::V3::MergeRequests
+ mount ::API::V3::Projects
+ mount ::API::V3::ProjectSnippets
+ end
before { allow_access_with_scope :api }
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 4ac491edc1b..13752eb4947 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -37,7 +37,7 @@ module API
end
desc 'Get the lists of a project board' do
- detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13'
+ detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
end
get '/lists' do
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index be659fa4a6a..9331be1f7de 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -84,7 +84,7 @@ module API
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
protected_branch = user_project.protected_branches.find_by(name: branch.name)
- protected_branch.destroy if protected_branch
+ protected_branch&.destroy
present branch, with: Entities::RepoBranch, project: user_project
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index af61be343be..44fe0fc4a95 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -209,7 +209,7 @@ module API
build = get_build!(params[:build_id])
- bad_request!("Unplayable Build") unless build.playable?
+ bad_request!("Unplayable Job") unless build.playable?
build.play(current_user)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index e6d707f3c3d..2fefe760d24 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -54,7 +54,7 @@ module API
authorize! :push_code, user_project
attrs = declared_params
- attrs[:source_branch] = attrs[:branch_name]
+ attrs[:start_branch] = attrs[:branch_name]
attrs[:target_branch] = attrs[:branch_name]
attrs[:actions].map! do |action|
action[:action] = action[:action].to_sym
@@ -139,8 +139,6 @@ module API
commit_params = {
commit: commit,
create_merge_request: false,
- source_project: user_project,
- source_branch: commit.cherry_pick_branch_name,
target_branch: params[:branch]
}
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 64da7d6b86f..3f5183d46a2 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,5 +1,4 @@
module API
- # Projects API
class DeployKeys < Grape::API
before { authenticate! }
@@ -16,107 +15,102 @@ module API
resource :projects do
before { authorize_admin_project }
- # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "projects/:id/deploy_keys/..." instead.
- #
- %w(keys deploy_keys).each do |path|
- desc "Get a specific project's deploy keys" do
- success Entities::SSHKey
- end
- get ":id/#{path}" do
- present user_project.deploy_keys, with: Entities::SSHKey
- end
+ desc "Get a specific project's deploy keys" do
+ success Entities::SSHKey
+ end
+ get ":id/deploy_keys" do
+ present user_project.deploy_keys, with: Entities::SSHKey
+ end
- desc 'Get single deploy key' do
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- get ":id/#{path}/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
+ desc 'Get single deploy key' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/deploy_keys" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
present key, with: Entities::SSHKey
+ break
end
- desc 'Add new deploy key to currently authenticated user' do
- success Entities::SSHKey
- end
- params do
- requires :key, type: String, desc: 'The new deploy key'
- requires :title, type: String, desc: 'The name of the deploy key'
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ break
end
- post ":id/#{path}" do
- params[:key].strip!
- # Check for an existing key joined to this project
- key = user_project.deploy_keys.find_by(key: params[:key])
- if key
- present key, with: Entities::SSHKey
- break
- end
-
- # Check for available deploy keys in other projects
- key = current_user.accessible_deploy_keys.find_by(key: params[:key])
- if key
- user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- break
- end
-
- # Create a new deploy key
- key = DeployKey.new(declared_params(include_missing: false))
- if key.valid? && user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- else
- render_validation_error!(key)
- end
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ else
+ render_validation_error!(key)
end
+ end
- desc 'Enable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- post ":id/#{path}/:key_id/enable" do
- key = ::Projects::EnableDeployKeyService.new(user_project,
- current_user, declared_params).execute
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/deploy_keys/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
- if key
- present key, with: Entities::SSHKey
- else
- not_found!('Deploy Key')
- end
+ if key
+ present key, with: Entities::SSHKey
+ else
+ not_found!('Deploy Key')
end
+ end
- desc 'Disable a deploy key for a project' do
- detail 'This feature was added in GitLab 8.11'
- success Entities::SSHKey
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id/disable" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- key.destroy
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/deploy_keys/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
- present key.deploy_key, with: Entities::SSHKey
- end
+ present key.deploy_key, with: Entities::SSHKey
+ end
- desc 'Delete deploy key for a project' do
- success Key
- end
- params do
- requires :key_id, type: Integer, desc: 'The ID of the deploy key'
- end
- delete ":id/#{path}/:key_id" do
- key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- if key
- key.destroy
- else
- not_found!('Deploy Key')
- end
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/deploy_keys/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
+ key.destroy
+ else
+ not_found!('Deploy Key')
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9f59939e9ae..2a071e649fa 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -137,6 +137,8 @@ module API
expose :avatar_url
expose :web_url
expose :request_access_enabled
+ expose :full_name, :full_path
+ expose :parent_id
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
@@ -212,9 +214,6 @@ module API
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
- # TODO (rspeicher): Deprecated; remove in 9.0
- expose(:expires_at) { |snippet| nil }
-
expose :web_url do |snippet, options|
Gitlab::UrlBuilder.build(snippet)
end
@@ -381,9 +380,7 @@ module API
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options|
- if event.author
- event.author.username
- end
+ event.author&.username
end
end
@@ -574,6 +571,7 @@ module API
expose :koding_url
expose :plantuml_enabled
expose :plantuml_url
+ expose :terminal_max_session_time
end
class Release < Grape::Entity
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 2e79e22e649..2ecdd747c8e 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -5,7 +5,7 @@ module API
def commit_params(attrs)
{
file_path: attrs[:file_path],
- source_branch: attrs[:branch_name],
+ start_branch: attrs[:branch_name],
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
@@ -117,7 +117,7 @@ module API
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
- result = ::Files::DeleteService.new(user_project, current_user, commit_params(file_params)).execute
+ result = ::Files::DestroyService.new(user_project, current_user, commit_params(file_params)).execute
if result[:status] == :success
status(200)
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 7682d286866..5c132bdd6f9 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -73,6 +73,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the group'
requires :path, type: String, desc: 'The path of the group'
+ optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group'
use :optional_params
end
post do
@@ -125,7 +126,7 @@ module API
delete ":id" do
group = find_group!(params[:id])
authorize! :admin_group, group
- DestroyGroupService.new(group, current_user).execute
+ ::Groups::DestroyService.new(group, current_user).execute
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a1d7b323f4f..dfab60f7fa5 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -45,7 +45,7 @@ module API
if id =~ /^\d+$/
Project.find_by(id: id)
else
- Project.find_with_namespace(id)
+ Project.find_by_full_path(id)
end
end
@@ -304,7 +304,7 @@ module API
header['X-Sendfile'] = path
body
else
- path
+ file path
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index e8975eb57e0..080a6274957 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -30,7 +30,7 @@ module API
def wiki?
@wiki ||= project_path.end_with?('.wiki') &&
- !Project.find_with_namespace(project_path)
+ !Project.find_by_full_path(project_path)
end
def project
@@ -41,7 +41,7 @@ module API
# the wiki repository as well.
project_path.chomp!('.wiki') if wiki?
- Project.find_with_namespace(project_path)
+ Project.find_by_full_path(project_path)
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index fe016c1ec0a..90fca20d4fa 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -15,8 +15,6 @@ module API
labels = args.delete(:labels)
args[:label_name] = labels if match_all_labels
- args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
-
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
# TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
@@ -97,7 +95,6 @@ module API
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
- optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
use :issues_params
end
get ":id/issues" do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index d85f1f78cd6..d1d78775c6d 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -56,16 +56,9 @@ module API
member = source.members.find_by(user_id: params[:user_id])
- # We need this explicit check because `source.add_user` doesn't
- # currently return the member created so it would return 201 even if
- # the member already existed...
- # The `source_type == 'group'` check is to ensure back-compatibility
- # but 409 behavior should be used for both project and group members in 9.0!
- conflict!('Member already exists') if source_type == 'group' && member
-
- unless member
- member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- end
+ conflict!('Member already exists') if member
+
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 7ffb38e62da..8e09a6f7354 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,8 +2,6 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze
-
before { authenticate! }
params do
@@ -46,14 +44,14 @@ module API
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: Array[Integer], desc: 'The IID of the merge requests'
+ optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
use :pagination
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests.inc_notes_with_associations
- merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+ merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present?
merge_requests =
case params[:state]
@@ -104,177 +102,167 @@ module API
merge_request.destroy
end
- # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0
- # Use "merge_requests/:merge_request_id/..." instead.
- #
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 = find_merge_request_with_access(params[:merge_request_id])
+ desc 'Get a single merge request' do
+ success Entities::MergeRequest
+ end
+ get ':id/merge_requests/:merge_request_id' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
- end
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- desc 'Get the commits of a merge request' do
- success Entities::RepoCommit
- end
- get "#{path}/commits" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ desc 'Get the commits of a merge request' do
+ success Entities::RepoCommit
+ end
+ get ':id/merge_requests/:merge_request_id/commits' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.commits, with: Entities::RepoCommit
- end
+ present merge_request.commits, with: Entities::RepoCommit
+ end
- desc 'Show the merge request changes' do
- success Entities::MergeRequestChanges
- end
- get "#{path}/changes" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
+ desc 'Show the merge request changes' do
+ success Entities::MergeRequestChanges
+ end
+ get ':id/merge_requests/:merge_request_id/changes' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
- end
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
+ end
- desc 'Update a merge request' do
- success Entities::MergeRequest
- end
- params do
- optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
- optional :target_branch, type: String, allow_blank: false, 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,
- :remove_source_branch
- end
- put path do
- merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+ desc 'Update a merge request' do
+ success Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
+ optional :state_event, type: String, values: %w[close reopen],
+ desc: 'Status of the merge request'
+ use :optional_params
+ at_least_one_of :title, :target_branch, :description, :assignee_id,
+ :milestone_id, :labels, :state_event,
+ :remove_source_branch
+ end
+ put ':id/merge_requests/:merge_request_id' do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
- mr_params = declared_params(include_missing: false)
- mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
- merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).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, project: user_project
- else
- handle_merge_request_errors! merge_request.errors
- end
+ if merge_request.valid?
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
end
+ end
- 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 pipeline succeeds'
- optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
- end
- put "#{path}/merge" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ 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 pipeline succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put ':id/merge_requests/:merge_request_id/merge' do
+ merge_request = find_project_merge_request(params[:merge_request_id])
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized! unless merge_request.can_be_merged_by?(current_user)
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
- not_allowed! unless merge_request.mergeable_state?
+ not_allowed! unless merge_request.mergeable_state?
- render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
- merge_params = {
- commit_message: params[:merge_commit_message],
- should_remove_source_branch: params[:should_remove_source_branch]
- }
-
- if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
- ::MergeRequests::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- else
- ::MergeRequests::MergeService
- .new(merge_request.target_project, current_user, merge_params)
- .execute(merge_request)
- end
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
end
- desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
- success Entities::MergeRequest
- end
- post "#{path}/cancel_merge_when_build_succeeds" do
- merge_request = find_project_merge_request(params[:merge_request_id])
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ end
- unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success Entities::MergeRequest
+ end
+ post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do
+ merge_request = find_project_merge_request(params[:merge_request_id])
- ::MergeRequest::MergeWhenPipelineSucceedsService
- .new(merge_request.target_project, current_user)
- .cancel(merge_request)
- end
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
- desc 'Get the comments of a merge request' do
- detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0'
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/comments" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
- end
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
- 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
- merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+ desc 'Get the comments of a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get ':id/merge_requests/:merge_request_id/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: Entities::MRNote
+ end
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
+ desc 'Post a comment to a merge request' do
+ success Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post ':id/merge_requests/:merge_request_id/comments' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
- desc 'List issues that will be closed on merge' do
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get "#{path}/closes_issues" do
- merge_request = find_merge_request_with_access(params[:merge_request_id])
- issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ if note.save
+ present note, with: Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
+
+ desc 'List issues that will be closed on merge' do
+ success Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get ':id/merge_requests/:merge_request_id/closes_issues' do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
end
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 4d2a8f48267..8beccaaabd1 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -131,7 +131,7 @@ module API
note = user_project.notes.find(params[:note_id])
authorize! :admin_note, note
- ::Notes::DeleteService.new(user_project, current_user).execute(note)
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
present note, with: Entities::Note
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 9d8c5b63685..dcc0c82ee27 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -58,7 +58,7 @@ module API
end
post ":id/snippets" do
authorize! :create_project_snippet, user_project
- snippet_params = declared_params
+ snippet_params = declared_params.merge(request: request, api: true)
snippet_params[:content] = snippet_params.delete(:code)
snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 941f47114a4..bd4b23195ac 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -16,7 +16,6 @@ module API
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
- optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
optional :visibility_level, type: Integer, values: [
Gitlab::VisibilityLevel::PRIVATE,
Gitlab::VisibilityLevel::INTERNAL,
@@ -26,16 +25,6 @@ module API
optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
end
-
- def map_public_to_visibility_level(attrs)
- publik = attrs.delete(:public)
- if !publik.nil? && !attrs[:visibility_level].present?
- # Since setting the public attribute to private could mean either
- # private or internal, use the more conservative option, private.
- attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
- end
- attrs
- end
end
resource :projects do
@@ -151,22 +140,6 @@ module API
present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
end
- desc 'Search for projects the current user has access to' do
- success Entities::Project
- end
- params do
- requires :query, type: String, desc: 'The project name to be searched'
- use :sort_params
- use :pagination
- end
- get "/search/:query", requirements: { query: /[^\/]+/ } do
- search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
- projects = search_service.objects('projects', params[:page])
- projects = projects.reorder(params[:order_by] => params[:sort])
-
- present paginate(projects), with: Entities::Project
- end
-
desc 'Create new project' do
success Entities::Project
end
@@ -177,7 +150,7 @@ module API
use :create_params
end
post do
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved?
@@ -206,7 +179,7 @@ module API
user = User.find_by(id: params.delete(:user_id))
not_found!('User') unless user
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
project = ::Projects::CreateService.new(user, attrs).execute
if project.saved?
@@ -284,14 +257,14 @@ module API
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
:shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :public, :visibility_level, :public_builds,
+ :lfs_enabled, :visibility_level, :public_builds,
:request_access_enabled, :only_allow_merge_if_build_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
end
put ':id' do
authorize_admin_project
- attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ attrs = declared_params(include_missing: false)
authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
diff --git a/lib/api/services.rb b/lib/api/services.rb
index a0abec49438..1456fe4688b 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -661,6 +661,14 @@ module API
end
trigger_services.each do |service_slug, settings|
+ helpers do
+ def chat_command_service(project, service_slug, params)
+ project.services.active.where(template: false).find do |service|
+ service.try(:token) == params[:token] && service.to_param == service_slug.underscore
+ end
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -679,9 +687,8 @@ module API
# This is not accurate, but done to prevent leakage of the project names
not_found!('Service') unless project
- service = project.find_or_initialize_service(service_slug.underscore)
-
- result = service.try(:active?) && service.try(:trigger, params)
+ service = chat_command_service(project, service_slug, params)
+ result = service.try(:trigger, params)
if result
status result[:status] || 200
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index c5eff16a5de..747ceb4e3e0 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -57,6 +57,7 @@ module API
requires :shared_runners_text, type: String, desc: 'Shared runners text '
end
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have"
+ optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
given metrics_enabled: ->(val) { val } do
@@ -107,6 +108,7 @@ module API
requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run."
requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
end
+ optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
:default_group_visibility, :restricted_visibility_levels, :import_sources,
:enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
@@ -115,12 +117,12 @@ module API
:send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled,
:after_sign_up_text, :signin_enabled, :require_two_factor_authentication,
:home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text,
- :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay,
+ :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay,
:metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled,
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled
+ :housekeeping_enabled, :terminal_max_session_time
end
put "application/settings" do
if current_settings.update_attributes(declared_params(include_missing: false))
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index e096e636806..eb9ece49e7f 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -64,7 +64,7 @@ module API
desc: 'The visibility level of the snippet'
end
post do
- attrs = declared_params(include_missing: false)
+ attrs = declared_params(include_missing: false).merge(request: request, api: true)
snippet = CreateSnippetService.new(nil, current_user, attrs).execute
if snippet.persisted?
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 11a7368b4c0..82ac3886ac3 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -82,7 +82,9 @@ module API
end
params do
requires :email, type: String, desc: 'The email of the user'
- requires :password, type: String, desc: 'The password of the new user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ at_least_one_of :password, :reset_password
requires :name, type: String, desc: 'The name of the user'
requires :username, type: String, desc: 'The username of the user'
use :optional_attributes
@@ -94,8 +96,18 @@ module API
user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid)
confirm = user_params.delete(:confirm)
+ user = User.new(user_params.except(:extern_uid, :provider, :reset_password))
+
+ if user_params.delete(:reset_password)
+ user.attributes = {
+ force_random_password: true,
+ password_expires_at: nil,
+ created_by_id: current_user.id
+ }
+ user.generate_password
+ user.generate_reset_token
+ end
- user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
if identity_attrs.any?
@@ -160,6 +172,8 @@ module API
end
end
+ user_params.merge!(password_expires_at: Time.now) if user_params[:password].present?
+
if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
else
@@ -291,7 +305,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- DeleteUserService.new(current_user).execute(user)
+ ::Users::DestroyService.new(current_user).execute(user)
end
desc 'Block a user. Available only for admins.'
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
new file mode 100644
index 00000000000..5bbb167755c
--- /dev/null
+++ b/lib/api/v3/deploy_keys.rb
@@ -0,0 +1,122 @@
+module API
+ module V3
+ class DeployKeys < Grape::API
+ before { authenticate! }
+
+ get "deploy_keys" do
+ authenticated_as_admin!
+
+ keys = DeployKey.all
+ present keys, with: ::API::Entities::SSHKey
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of the project'
+ end
+ resource :projects do
+ before { authorize_admin_project }
+
+ %w(keys deploy_keys).each do |path|
+ desc "Get a specific project's deploy keys" do
+ success ::API::Entities::SSHKey
+ end
+ get ":id/#{path}" do
+ present user_project.deploy_keys, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Get single deploy key' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Add new deploy key to currently authenticated user' do
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new deploy key'
+ requires :title, type: String, desc: 'The name of the deploy key'
+ end
+ post ":id/#{path}" do
+ params[:key].strip!
+
+ # Check for an existing key joined to this project
+ key = user_project.deploy_keys.find_by(key: params[:key])
+ if key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Check for available deploy keys in other projects
+ key = current_user.accessible_deploy_keys.find_by(key: params[:key])
+ if key
+ user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ break
+ end
+
+ # Create a new deploy key
+ key = DeployKey.new(declared_params(include_missing: false))
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: ::API::Entities::SSHKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/#{path}/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared_params).execute
+
+ if key
+ present key, with: ::API::Entities::SSHKey
+ else
+ not_found!('Deploy Key')
+ end
+ end
+
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success ::API::Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
+
+ present key.deploy_key, with: ::API::Entities::SSHKey
+ end
+
+ desc 'Delete deploy key for a project' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ if key
+ key.destroy
+ else
+ not_found!('Deploy Key')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
new file mode 100644
index 00000000000..3cc0dc968a8
--- /dev/null
+++ b/lib/api/v3/entities.rb
@@ -0,0 +1,16 @@
+module API
+ module V3
+ module Entities
+ class ProjectSnippet < Grape::Entity
+ expose :id, :title, :file_name
+ expose :author, using: ::API::Entities::UserBasic
+ expose :updated_at, :created_at
+ expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ Gitlab::UrlBuilder.build(snippet)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
new file mode 100644
index 00000000000..081d45165e8
--- /dev/null
+++ b/lib/api/v3/issues.rb
@@ -0,0 +1,231 @@
+module API
+ module V3
+ class Issues < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers do
+ def find_issues(args = {})
+ args = params.merge(args)
+
+ args.delete(:id)
+ args[:milestone_title] = args.delete(:milestone)
+
+ match_all_labels = args.delete(:match_all_labels)
+ labels = args.delete(:labels)
+ args[:label_name] = labels if match_all_labels
+
+ args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid)
+
+ issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
+
+ if !match_all_labels && labels.present?
+ issues = issues.includes(:labels).where('labels.title' => labels.split(','))
+ end
+
+ issues.reorder(args[:order_by] => args[:sort])
+ end
+
+ params :issues_params do
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Milestone title'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return issues sorted in `asc` or `desc` order.'
+ optional :milestone, type: String, desc: 'Return issues for a specific milestone'
+ use :pagination
+ end
+
+ params :issue_params do
+ optional :description, type: String, desc: 'The description of an issue'
+ optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue'
+ optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
+ optional :labels, type: String, desc: 'Comma-separated list of label names'
+ optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY'
+ optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
+ end
+ end
+
+ resource :issues do
+ desc "Get currently authenticated user's issues" do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get do
+ issues = find_issues(scope: 'authored')
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups do
+ desc 'Get a list of group issues' do
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ desc: 'Return opened, closed, or all issues'
+ use :issues_params
+ end
+ get ":id/issues" do
+ group = find_group!(params[:id])
+
+ issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ desc 'Get a list of project issues' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::Entities::Issue
+ end
+ params do
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :iid, type: Integer, desc: 'Return the issue having the given `iid`'
+ use :issues_params
+ end
+ get ":id/issues" do
+ project = find_project(params[:id])
+
+ issues = find_issues(project_id: project.id)
+
+ present paginate(issues), with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Get a single project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ get ":id/issues/:issue_id" do
+ issue = find_project_issue(params[:issue_id])
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a new project issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :title, type: String, desc: 'The title of an issue'
+ optional :created_at, type: DateTime,
+ desc: 'Date time when the issue was created. Available only for admins and project owners.'
+ optional :merge_request_for_resolving_discussions, type: Integer,
+ desc: 'The IID of a merge request for which to resolve discussions'
+ use :issue_params
+ end
+ post ':id/issues' do
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:created_at)
+ end
+
+ issue_params = declared_params(include_missing: false)
+
+ if merge_request_iid = params[:merge_request_for_resolving_discussions]
+ issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id).
+ execute.
+ find_by(iid: merge_request_iid)
+ end
+
+ issue = ::Issues::CreateService.new(user_project,
+ current_user,
+ issue_params.merge(request: request, api: true)).execute
+ if issue.spam?
+ render_api_error!({ error: 'Spam detected' }, 400)
+ end
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Update an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ optional :title, type: String, desc: 'The title of an issue'
+ optional :updated_at, type: DateTime,
+ desc: 'Date time when the issue was updated. Available only for admins and project owners.'
+ optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue'
+ use :issue_params
+ at_least_one_of :title, :description, :assignee_id, :milestone_id,
+ :labels, :created_at, :due_date, :confidential, :state_event
+ end
+ put ':id/issues/:issue_id' do
+ issue = user_project.issues.find(params.delete(:issue_id))
+ authorize! :update_issue, issue
+
+ # Setting created_at time only allowed for admins and project owners
+ unless current_user.admin? || user_project.owner == current_user
+ params.delete(:updated_at)
+ end
+
+ issue = ::Issues::UpdateService.new(user_project,
+ current_user,
+ declared_params(include_missing: false)).execute(issue)
+
+ if issue.valid?
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_validation_error!(issue)
+ end
+ end
+
+ desc 'Move an existing issue' do
+ success ::API::Entities::Issue
+ end
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ requires :to_project_id, type: Integer, desc: 'The ID of the new project'
+ end
+ post ':id/issues/:issue_id/move' do
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
+
+ new_project = Project.find_by(id: params[:to_project_id])
+ not_found!('Project') unless new_project
+
+ begin
+ issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project)
+ present issue, with: ::API::Entities::Issue, current_user: current_user, project: user_project
+ rescue ::Issues::MoveService::MoveError => error
+ render_api_error!(error.message, 400)
+ end
+ end
+
+ desc 'Delete a project issue'
+ params do
+ requires :issue_id, type: Integer, desc: 'The ID of a project issue'
+ end
+ delete ":id/issues/:issue_id" do
+ issue = user_project.issues.find_by(id: params[:issue_id])
+ not_found!('Issue') unless issue
+
+ authorize!(:destroy_issue, issue)
+ issue.destroy
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
new file mode 100644
index 00000000000..4e6cb2e3c52
--- /dev/null
+++ b/lib/api/v3/members.rb
@@ -0,0 +1,134 @@
+module API
+ module V3
+ class Members < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ helpers ::API::Helpers::MembersHelpers
+
+ %w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
+ resource source_type.pluralize do
+ desc 'Gets a list of group or project members viewable by the authenticated user.' do
+ success ::API::Entities::Member
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ use :pagination
+ end
+ get ":id/members" do
+ source = find_source(source_type, params[:id])
+
+ users = source.users
+ users = users.merge(User.search(params[:query])) if params[:query]
+
+ present paginate(users), with: ::API::Entities::Member, source: source
+ end
+
+ desc 'Gets a member of a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
+ get ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+
+ members = source.members
+ member = members.find_by!(user_id: params[:user_id])
+
+ present member.user, with: ::API::Entities::Member, member: member
+ end
+
+ desc 'Adds a member to a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
+ post ":id/members" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+
+ member = source.members.find_by(user_id: params[:user_id])
+
+ # We need this explicit check because `source.add_user` doesn't
+ # currently return the member created so it would return 201 even if
+ # the member already existed...
+ # The `source_type == 'group'` check is to ensure back-compatibility
+ # but 409 behavior should be used for both project and group members in 9.0!
+ conflict!('Member already exists') if source_type == 'group' && member
+
+ unless member
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ end
+ if member.persisted? && member.valid?
+ present member.user, with: ::API::Entities::Member, member: member
+ else
+ # This is to ensure back-compatibility but 400 behavior should be used
+ # for all validation errors in 9.0!
+ render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+ render_validation_error!(member)
+ end
+ end
+
+ desc 'Updates a member of a group or project.' do
+ success ::API::Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
+ put ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+
+ member = source.members.find_by!(user_id: params[:user_id])
+ attrs = attributes_for_keys [:access_level, :expires_at]
+
+ if member.update_attributes(attrs)
+ present member.user, with: ::API::Entities::Member, member: member
+ else
+ # This is to ensure back-compatibility but 400 behavior should be used
+ # for all validation errors in 9.0!
+ render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+ render_validation_error!(member)
+ end
+ end
+
+ desc 'Removes a user from a group or project.'
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
+ delete ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+
+ # This is to ensure back-compatibility but find_by! should be used
+ # in that casse in 9.0!
+ member = source.members.find_by(user_id: params[:user_id])
+
+ # This is to ensure back-compatibility but this should be removed in
+ # favor of find_by! in 9.0!
+ not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
+
+ # This is to ensure back-compatibility but 204 behavior should be used
+ # for all DELETE endpoints in 9.0!
+ if member.nil?
+ { message: "Access revoked", id: params[:user_id].to_i }
+ else
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
+
+ present member.user, with: ::API::Entities::Member, member: member
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
new file mode 100644
index 00000000000..129f9d850e9
--- /dev/null
+++ b/lib/api/v3/merge_requests.rb
@@ -0,0 +1,280 @@
+module API
+ module V3
+ class MergeRequests < Grape::API
+ include PaginationParams
+
+ DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ include TimeTrackingEndpoints
+
+ helpers do
+ def handle_merge_request_errors!(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ elsif errors[:branch_conflict].any?
+ error!(errors[:branch_conflict], 422)
+ elsif errors[:validate_fork].any?
+ error!(errors[:validate_fork], 422)
+ elsif errors[:validate_branches].any?
+ conflict!(errors[:validate_branches])
+ end
+
+ 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'
+ optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
+ end
+ end
+
+ desc 'List merge requests' do
+ detail 'iid filter is deprecated have been removed on V4'
+ success ::API::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: Array[Integer], desc: 'The IID of the merge requests'
+ use :pagination
+ end
+ get ":id/merge_requests" do
+ authorize! :read_merge_request, user_project
+
+ merge_requests = user_project.merge_requests.inc_notes_with_associations
+ merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present?
+
+ merge_requests =
+ 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(params[:order_by] => params[:sort])
+ present paginate(merge_requests), with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Create a merge request' do
+ success ::API::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
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
+
+ if merge_request.valid?
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ 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 = find_project_merge_request(params[:merge_request_id])
+
+ authorize!(:destroy_merge_request, merge_request)
+ merge_request.destroy
+ end
+
+ 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 ::API::Entities::MergeRequest
+ end
+ get path do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Get the commits of a merge request' do
+ success ::API::Entities::RepoCommit
+ end
+ get "#{path}/commits" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request.commits, with: ::API::Entities::RepoCommit
+ end
+
+ desc 'Show the merge request changes' do
+ success ::API::Entities::MergeRequestChanges
+ end
+ get "#{path}/changes" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+
+ present merge_request, with: ::API::Entities::MergeRequestChanges, current_user: current_user
+ end
+
+ desc 'Update a merge request' do
+ success ::API::Entities::MergeRequest
+ end
+ params do
+ optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
+ optional :target_branch, type: String, allow_blank: false, 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,
+ :remove_source_branch
+ end
+ put path do
+ merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
+
+ mr_params = declared_params(include_missing: false)
+ mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+
+ merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
+
+ if merge_request.valid?
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ else
+ handle_merge_request_errors! merge_request.errors
+ end
+ end
+
+ desc 'Merge a merge request' do
+ success ::API::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 pipeline succeeds'
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+ end
+ put "#{path}/merge" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ # Merge request can not be merged
+ # because user dont have permissions to push into target branch
+ unauthorized! unless merge_request.can_be_merged_by?(current_user)
+
+ not_allowed! unless merge_request.mergeable_state?
+
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable?
+
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active?
+ ::MergeRequests::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ else
+ ::MergeRequests::MergeService
+ .new(merge_request.target_project, current_user, merge_params)
+ .execute(merge_request)
+ end
+
+ present merge_request, with: ::API::Entities::MergeRequest, current_user: current_user, project: user_project
+ end
+
+ desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do
+ success ::API::Entities::MergeRequest
+ end
+ post "#{path}/cancel_merge_when_build_succeeds" do
+ merge_request = find_project_merge_request(params[:merge_request_id])
+
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+
+ ::MergeRequest::MergeWhenPipelineSucceedsService
+ .new(merge_request.target_project, current_user)
+ .cancel(merge_request)
+ end
+
+ desc 'Get the comments of a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ present paginate(merge_request.notes.fresh), with: ::API::Entities::MRNote
+ end
+
+ desc 'Post a comment to a merge request' do
+ detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4'
+ success ::API::Entities::MRNote
+ end
+ params do
+ requires :note, type: String, desc: 'The text of the comment'
+ end
+ post "#{path}/comments" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
+
+ opts = {
+ note: params[:note],
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id
+ }
+
+ note = ::Notes::CreateService.new(user_project, current_user, opts).execute
+
+ if note.save
+ present note, with: ::API::Entities::MRNote
+ else
+ render_api_error!("Failed to save note #{note.errors.messages}", 400)
+ end
+ end
+
+ desc 'List issues that will be closed on merge' do
+ success ::API::Entities::MRNote
+ end
+ params do
+ use :pagination
+ end
+ get "#{path}/closes_issues" do
+ merge_request = find_merge_request_with_access(params[:merge_request_id])
+ issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
+ present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
new file mode 100644
index 00000000000..9f95d4395fa
--- /dev/null
+++ b/lib/api/v3/project_snippets.rb
@@ -0,0 +1,135 @@
+module API
+ module V3
+ class ProjectSnippets < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects do
+ helpers do
+ def handle_project_member_errors(errors)
+ if errors[:project_access].any?
+ error!(errors[:project_access], 422)
+ end
+ not_found!
+ end
+
+ def snippets_for_current_user
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params)
+ end
+ end
+
+ desc 'Get all project snippets' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ use :pagination
+ end
+ get ":id/snippets" do
+ present paginate(snippets_for_current_user), with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Get a single project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find(params[:snippet_id])
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ end
+
+ desc 'Create a new project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the snippet'
+ requires :file_name, type: String, desc: 'The file name of the snippet'
+ requires :code, type: String, desc: 'The content of the snippet'
+ requires :visibility_level, type: Integer,
+ values: [Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC],
+ desc: 'The visibility level of the snippet'
+ end
+ post ":id/snippets" do
+ authorize! :create_project_snippet, user_project
+ snippet_params = declared_params.merge(request: request, api: true)
+ snippet_params[:content] = snippet_params.delete(:code)
+
+ snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Update an existing project snippet' do
+ success ::API::V3::Entities::ProjectSnippet
+ end
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ optional :title, type: String, desc: 'The title of the snippet'
+ optional :file_name, type: String, desc: 'The file name of the snippet'
+ optional :code, type: String, desc: 'The content of the snippet'
+ optional :visibility_level, type: Integer,
+ values: [Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC],
+ desc: 'The visibility level of the snippet'
+ at_least_one_of :title, :file_name, :code, :visibility_level
+ end
+ put ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
+ not_found!('Snippet') unless snippet
+
+ authorize! :update_project_snippet, snippet
+
+ snippet_params = declared_params(include_missing: false)
+ snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
+
+ UpdateSnippetService.new(user_project, current_user, snippet,
+ snippet_params).execute
+
+ if snippet.persisted?
+ present snippet, with: ::API::V3::Entities::ProjectSnippet
+ else
+ render_validation_error!(snippet)
+ end
+ end
+
+ desc 'Delete a project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ delete ":id/snippets/:snippet_id" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ authorize! :admin_project_snippet, snippet
+ snippet.destroy
+ end
+
+ desc 'Get a raw project snippet'
+ params do
+ requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
+ end
+ get ":id/snippets/:snippet_id/raw" do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet
+
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ present snippet.content
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
new file mode 100644
index 00000000000..6796da83f07
--- /dev/null
+++ b/lib/api/v3/projects.rb
@@ -0,0 +1,458 @@
+module API
+ module V3
+ class Projects < Grape::API
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the project'
+ optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
+ optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled'
+ optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled'
+ optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
+ optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
+ optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
+ optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
+ optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
+ optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :visibility_level, type: Integer, values: [
+ Gitlab::VisibilityLevel::PRIVATE,
+ Gitlab::VisibilityLevel::INTERNAL,
+ Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.'
+ optional :public_builds, type: Boolean, desc: 'Perform public builds'
+ optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
+ optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
+ optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
+ end
+
+ def map_public_to_visibility_level(attrs)
+ publik = attrs.delete(:public)
+ if !publik.nil? && !attrs[:visibility_level].present?
+ # Since setting the public attribute to private could mean either
+ # private or internal, use the more conservative option, private.
+ attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
+ end
+ attrs
+ end
+ end
+
+ resource :projects do
+ helpers do
+ params :collection_params do
+ use :sort_params
+ use :filter_params
+ use :pagination
+
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ end
+
+ params :sort_params do
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ end
+
+ params :filter_params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: %w[public internal private],
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ end
+
+ params :statistics_params do
+ optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
+ end
+
+ params :create_params do
+ optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
+ optional :import_url, type: String, desc: 'URL from which the project is imported'
+ end
+
+ def present_projects(projects, options = {})
+ options = options.reverse_merge(
+ with: ::API::Entities::Project,
+ current_user: current_user,
+ simple: params[:simple],
+ )
+
+ projects = filter_projects(projects)
+ projects = projects.with_statistics if options[:statistics]
+ options[:with] = ::API::Entities::BasicProjectDetails if options[:simple]
+
+ present paginate(projects), options
+ end
+ end
+
+ desc 'Get a list of visible projects for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/visible' do
+ entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present_projects ProjectsFinder.new.execute(current_user), with: entity
+ end
+
+ desc 'Get a projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get do
+ authenticate!
+
+ present_projects current_user.authorized_projects,
+ with: ::API::Entities::ProjectWithAccess
+ end
+
+ desc 'Get an owned projects list for authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/owned' do
+ authenticate!
+
+ present_projects current_user.owned_projects,
+ with: ::API::Entities::ProjectWithAccess,
+ statistics: params[:statistics]
+ end
+
+ desc 'Gets starred project for the authenticated user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ end
+ get '/starred' do
+ authenticate!
+
+ present_projects current_user.viewable_starred_projects
+ end
+
+ desc 'Get all projects for admin user' do
+ success ::API::Entities::BasicProjectDetails
+ end
+ params do
+ use :collection_params
+ use :statistics_params
+ end
+ get '/all' do
+ authenticated_as_admin!
+
+ present_projects Project.all, with: ::API::Entities::ProjectWithAccess, statistics: params[:statistics]
+ end
+
+ desc 'Search for projects the current user has access to' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :query, type: String, desc: 'The project name to be searched'
+ use :sort_params
+ use :pagination
+ end
+ get "/search/:query", requirements: { query: /[^\/]+/ } do
+ search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
+ projects = search_service.objects('projects', params[:page])
+ projects = projects.reorder(params[:order_by] => params[:sort])
+
+ present paginate(projects), with: ::API::Entities::Project
+ end
+
+ desc 'Create new project' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ use :optional_params
+ use :create_params
+ end
+ post do
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(current_user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ if project.errors[:limit_reached].present?
+ error!(project.errors[:limit_reached], 403)
+ end
+ render_validation_error!(project)
+ end
+ end
+
+ desc 'Create new project for a specified user. Only available to admin users.' do
+ success ::API::Entities::Project
+ end
+ params do
+ requires :name, type: String, desc: 'The name of the project'
+ requires :user_id, type: Integer, desc: 'The ID of a user'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ use :optional_params
+ use :create_params
+ end
+ post "user/:user_id" do
+ authenticated_as_admin!
+ user = User.find_by(id: params.delete(:user_id))
+ not_found!('User') unless user
+
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ project = ::Projects::CreateService.new(user, attrs).execute
+
+ if project.saved?
+ present project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, project)
+ else
+ render_validation_error!(project)
+ end
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: /[^\/]+/ } do
+ desc 'Get a single project' do
+ success ::API::Entities::ProjectWithAccess
+ end
+ get ":id" do
+ entity = current_user ? ::API::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
+ present user_project, with: entity, current_user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ end
+
+ desc 'Get events for a single project' do
+ success ::API::Entities::Event
+ end
+ params do
+ use :pagination
+ end
+ get ":id/events" do
+ present paginate(user_project.events.recent), with: ::API::Entities::Event
+ end
+
+ desc 'Fork new project for the current user or provided namespace.' do
+ success ::API::Entities::Project
+ end
+ params do
+ optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
+ end
+ post 'fork/:id' do
+ fork_params = declared_params(include_missing: false)
+ namespace_id = fork_params[:namespace]
+
+ if namespace_id.present?
+ fork_params[:namespace] = if namespace_id =~ /^\d+$/
+ Namespace.find_by(id: namespace_id)
+ else
+ Namespace.find_by_path_or_name(namespace_id)
+ end
+
+ unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
+ not_found!('Target Namespace')
+ end
+ end
+
+ forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute
+
+ if forked_project.errors.any?
+ conflict!(forked_project.errors.messages)
+ else
+ present forked_project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, forked_project)
+ end
+ end
+
+ desc 'Update an existing project' do
+ success ::API::Entities::Project
+ end
+ params do
+ optional :name, type: String, desc: 'The name of the project'
+ optional :default_branch, type: String, desc: 'The default branch of the project'
+ optional :path, type: String, desc: 'The path of the repository'
+ use :optional_params
+ at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
+ :wiki_enabled, :builds_enabled, :snippets_enabled,
+ :shared_runners_enabled, :container_registry_enabled,
+ :lfs_enabled, :public, :visibility_level, :public_builds,
+ :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :only_allow_merge_if_all_discussions_are_resolved, :path,
+ :default_branch
+ end
+ put ':id' do
+ authorize_admin_project
+ attrs = map_public_to_visibility_level(declared_params(include_missing: false))
+ authorize! :rename_project, user_project if attrs[:name].present?
+ authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
+
+ result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ present user_project, with: ::API::Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
+ else
+ render_validation_error!(user_project)
+ end
+ end
+
+ desc 'Archive a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/archive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.archive!
+
+ present user_project, with: ::API::Entities::Project
+ end
+
+ desc 'Unarchive a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/unarchive' do
+ authorize!(:archive_project, user_project)
+
+ user_project.unarchive!
+
+ present user_project, with: ::API::Entities::Project
+ end
+
+ desc 'Star a project' do
+ success ::API::Entities::Project
+ end
+ post ':id/star' do
+ if current_user.starred?(user_project)
+ not_modified!
+ else
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::Entities::Project
+ end
+ end
+
+ desc 'Unstar a project' do
+ success ::API::Entities::Project
+ end
+ delete ':id/star' do
+ if current_user.starred?(user_project)
+ current_user.toggle_star(user_project)
+ user_project.reload
+
+ present user_project, with: ::API::Entities::Project
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Remove a project'
+ delete ":id" do
+ authorize! :remove_project, user_project
+ ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+ end
+
+ desc 'Mark this project as forked from another'
+ params do
+ requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from'
+ end
+ post ":id/fork/:forked_from_id" do
+ authenticated_as_admin!
+
+ forked_from_project = find_project!(params[:forked_from_id])
+ not_found!("Source Project") unless forked_from_project
+
+ if user_project.forked_from_project.nil?
+ user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+ else
+ render_api_error!("Project already forked", 409)
+ end
+ end
+
+ desc 'Remove a forked_from relationship'
+ delete ":id/fork" do
+ authorize! :remove_fork_project, user_project
+
+ if user_project.forked?
+ user_project.forked_project_link.destroy
+ else
+ not_modified!
+ end
+ end
+
+ desc 'Share the project with a group' do
+ success ::API::Entities::ProjectGroupLink
+ end
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of a group'
+ requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level'
+ optional :expires_at, type: Date, desc: 'Share expiration date'
+ end
+ post ":id/share" do
+ authorize! :admin_project, user_project
+ group = Group.find_by_id(params[:group_id])
+
+ unless group && can?(current_user, :read_group, group)
+ not_found!('Group')
+ end
+
+ unless user_project.allowed_to_share_with_group?
+ return render_api_error!("The project sharing with group is disabled", 400)
+ end
+
+ link = user_project.project_group_links.new(declared_params(include_missing: false))
+
+ if link.save
+ present link, with: ::API::Entities::ProjectGroupLink
+ else
+ render_api_error!(link.errors.full_messages.first, 409)
+ end
+ end
+
+ params do
+ requires :group_id, type: Integer, desc: 'The ID of the group'
+ end
+ delete ":id/share/:group_id" do
+ authorize! :admin_project, user_project
+
+ link = user_project.project_group_links.find_by(group_id: params[:group_id])
+ not_found!('Group Link') unless link
+
+ link.destroy
+ no_content!
+ end
+
+ desc 'Upload a file'
+ params do
+ requires :file, type: File, desc: 'The file to be uploaded'
+ end
+ post ":id/uploads" do
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
+ desc 'Get the users list of a project' do
+ success ::API::Entities::UserBasic
+ end
+ params do
+ optional :search, type: String, desc: 'Return list of users matching the search criteria'
+ use :pagination
+ end
+ get ':id/users' do
+ users = user_project.team.users
+ users = users.search(params[:search]) if params[:search].present?
+
+ present paginate(users), with: ::API::Entities::UserBasic
+ end
+ end
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index cefbfdce3bb..f099c0651ac 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,6 +1,6 @@
module Backup
class Manager
- ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry]
+ ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry]
FOLDERS_TO_BACKUP = %w[repositories db]
FILE_NAME_SUFFIX = '_gitlab_backup.tar'
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
new file mode 100644
index 00000000000..215ded93bfe
--- /dev/null
+++ b/lib/backup/pages.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Pages < Files
+ def initialize
+ super('pages', Gitlab.config.pages.path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index 0257848b6bc..e2b57adf611 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -14,7 +14,7 @@ module Banzai
def project_from_ref(ref)
return context[:project] unless ref
- Project.find_with_namespace(ref)
+ Project.find_by_full_path(ref)
end
end
end
diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb
new file mode 100644
index 00000000000..e194cf59275
--- /dev/null
+++ b/lib/banzai/filter/plantuml_filter.rb
@@ -0,0 +1,39 @@
+require "nokogiri"
+require "asciidoctor-plantuml/plantuml"
+
+module Banzai
+ module Filter
+ # HTML that replaces all `code plantuml` tags with PlantUML img tags.
+ #
+ class PlantumlFilter < HTML::Pipeline::Filter
+ def call
+ return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled
+
+ plantuml_setup
+
+ doc.css('pre.plantuml').each do |el|
+ img_tag = Nokogiri::HTML::DocumentFragment.parse(
+ Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {}))
+ el.replace img_tag
+ end
+
+ doc
+ end
+
+ private
+
+ def settings
+ ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ end
+
+ def plantuml_setup
+ Asciidoctor::PlantUml.configure do |conf|
+ conf.url = settings.plantuml_url
+ conf.png_enable = settings.plantuml_enabled
+ conf.svg_enable = false
+ conf.txt_enable = false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index ac95a79009b..b25d6f18d59 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -10,6 +10,7 @@ module Banzai
def self.filters
@filters ||= FilterArray[
Filter::SyntaxHighlightFilter,
+ Filter::PlantumlFilter,
Filter::SanitizationFilter,
Filter::MathFilter,
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
index 1e1b51e683e..fb2faae02bc 100644
--- a/lib/banzai/querying.rb
+++ b/lib/banzai/querying.rb
@@ -1,18 +1,64 @@
module Banzai
module Querying
+ module_function
+
# Searches a Nokogiri document using a CSS query, optionally optimizing it
# whenever possible.
#
- # document - A document/element to search.
- # query - The CSS query to use.
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ # reference_options - A hash with nodes filter options
#
- # Returns a Nokogiri::XML::NodeSet.
- def self.css(document, query)
+ # Returns an array of Nokogiri::XML::Element objects if location is specified
+ # in reference_options. Otherwise it would a Nokogiri::XML::NodeSet.
+ def css(document, query, reference_options = {})
# When using "a.foo" Nokogiri compiles this to "//a[...]" but
# "descendant::a[...]" is quite a bit faster and achieves the same result.
xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+ xpath = restrict_to_p_nodes_at_root(xpath) if filter_nodes_at_beginning?(reference_options)
+ nodes = document.xpath(xpath)
+
+ filter_nodes(nodes, reference_options)
+ end
+
+ def restrict_to_p_nodes_at_root(xpath)
+ xpath.gsub('descendant::', './p/')
+ end
+
+ def filter_nodes(nodes, reference_options)
+ if filter_nodes_at_beginning?(reference_options)
+ filter_nodes_at_beginning(nodes)
+ else
+ nodes
+ end
+ end
+
+ def filter_nodes_at_beginning?(reference_options)
+ reference_options && reference_options[:location] == :beginning
+ end
+
+ # Selects child nodes if they are present in the beginning among other siblings.
+ #
+ # nodes - A Nokogiri::XML::NodeSet.
+ #
+ # Returns an array of Nokogiri::XML::Element objects.
+ def filter_nodes_at_beginning(nodes)
+ parents_and_nodes = nodes.group_by(&:parent)
+ filtered_nodes = []
+
+ parents_and_nodes.each do |parent, nodes|
+ children = parent.children
+ nodes = nodes.to_a
+
+ children.each do |child|
+ next if child.text.blank?
+ node = nodes.shift
+ break unless node == child
+ filtered_nodes << node
+ end
+ end
- document.xpath(xpath)
+ filtered_nodes
end
end
end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index b26a41a1f3b..8e3b0c4db79 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -16,6 +16,11 @@ module Banzai
processor.process(html_documents)
end
+ def reset_memoized_values
+ @html_documents = nil
+ @texts_and_contexts = []
+ end
+
private
def html_documents
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index d8a855ec1fe..2058a58d0ae 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -33,7 +33,7 @@ module Banzai
# they have access to.
class BaseParser
class << self
- attr_accessor :reference_type
+ attr_accessor :reference_type, :reference_options
end
# Returns the attribute name containing the value for every object to be
@@ -182,9 +182,10 @@ module Banzai
# the references.
def process(documents)
type = self.class.reference_type
+ reference_options = self.class.reference_options
nodes = documents.flat_map do |document|
- Querying.css(document, "a[data-reference-type='#{type}'].gfm").to_a
+ Querying.css(document, "a[data-reference-type='#{type}'].gfm", reference_options).to_a
end
gather_references(nodes)
diff --git a/lib/banzai/reference_parser/directly_addressed_user_parser.rb b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
new file mode 100644
index 00000000000..77df9bbd024
--- /dev/null
+++ b/lib/banzai/reference_parser/directly_addressed_user_parser.rb
@@ -0,0 +1,8 @@
+module Banzai
+ module ReferenceParser
+ class DirectlyAddressedUserParser < UserParser
+ self.reference_type = :user
+ self.reference_options = { location: :beginning }
+ end
+ end
+end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 7463bd719d5..649ee4d018b 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -61,6 +61,7 @@ module Ci
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
environment: job[:environment_name],
+ coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 730b05bed97..a10b4657d7d 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -8,6 +8,6 @@ class ProjectUrlConstrainer
return false
end
- Project.find_with_namespace(full_path).present?
+ Project.find_by_full_path(full_path).present?
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8dda65c71ef..f638905a1e0 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,16 @@ module Gitlab
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
+ # `user_with_password_for_git` should be the last check
+ # because it's the most expensive, especially when LDAP
+ # is enabled.
result =
service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
- user_with_password_for_git(login, password) ||
- oauth_access_token_check(login, password) ||
lfs_token_check(login, password) ||
+ oauth_access_token_check(login, password) ||
personal_access_token_check(login, password) ||
+ user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
@@ -143,7 +146,9 @@ module Gitlab
read_authentication_abilities
end
- Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
+ if Devise.secure_compare(token_handler.token, password)
+ Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities)
+ end
end
def build_access_token_check(login, password)
diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb
index 4fe53ce93a9..25da8474e95 100644
--- a/lib/gitlab/chat_commands/base_command.rb
+++ b/lib/gitlab/chat_commands/base_command.rb
@@ -42,10 +42,6 @@ module Gitlab
def find_by_iid(iid)
collection.find_by(iid: iid)
end
-
- def presenter
- Gitlab::ChatCommands::Presenter.new
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb
index 145086755e4..f34ed0f4cf2 100644
--- a/lib/gitlab/chat_commands/command.rb
+++ b/lib/gitlab/chat_commands/command.rb
@@ -3,7 +3,7 @@ module Gitlab
class Command < BaseCommand
COMMANDS = [
Gitlab::ChatCommands::IssueShow,
- Gitlab::ChatCommands::IssueCreate,
+ Gitlab::ChatCommands::IssueNew,
Gitlab::ChatCommands::IssueSearch,
Gitlab::ChatCommands::Deploy,
].freeze
@@ -13,51 +13,32 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- present command.new(project, current_user, params).execute(match)
+ command.new(project, current_user, params).execute(match)
else
- access_denied
+ Gitlab::ChatCommands::Presenters::Access.new.access_denied
end
else
- help(help_messages)
+ Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
end
end
def match_command
match = nil
- service = available_commands.find do |klass|
- match = klass.match(command)
- end
+ service =
+ available_commands.find do |klass|
+ match = klass.match(params[:text])
+ end
[service, match]
end
private
- 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)
- presenter.help(messages, params[:command])
- end
-
- def access_denied
- presenter.access_denied
- end
-
- def present(resource)
- presenter.present(resource)
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb
index 7127d2f6d04..458d90f84e8 100644
--- a/lib/gitlab/chat_commands/deploy.rb
+++ b/lib/gitlab/chat_commands/deploy.rb
@@ -1,8 +1,6 @@
module Gitlab
module ChatCommands
class Deploy < BaseCommand
- include Gitlab::Routing.url_helpers
-
def self.match(text)
/\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text)
end
@@ -24,35 +22,29 @@ module Gitlab
to = match[:to]
actions = find_actions(from, to)
- return unless actions.present?
- if actions.one?
- play!(from, to, actions.first)
+ if actions.none?
+ Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions
+ elsif actions.one?
+ action = play!(from, to, actions.first)
+ Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to)
else
- Result.new(:error, 'Too many actions defined')
+ Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions
end
end
private
def play!(from, to, action)
- new_action = action.play(current_user)
-
- Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.")
+ action.play(current_user)
end
def find_actions(from, to)
environment = project.environments.find_by(name: from)
- return unless environment
+ return [] unless environment
environment.actions_for(to).select(&:starts_environment?)
end
-
- def url(subject)
- polymorphic_url(
- [subject.project.namespace.becomes(Namespace), subject.project, subject]
- )
- end
end
end
end
diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb
new file mode 100644
index 00000000000..6c0e4d304a4
--- /dev/null
+++ b/lib/gitlab/chat_commands/help.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module ChatCommands
+ class Help < BaseCommand
+ # This class has to be used last, as it always matches. It has to match
+ # because other commands were not triggered and we want to show the help
+ # command
+ def self.match(_text)
+ true
+ end
+
+ def self.help_message
+ 'help'
+ end
+
+ def self.allowed?(_project, _user)
+ true
+ end
+
+ def execute(commands, text)
+ Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text)
+ end
+
+ def trigger
+ params[:command]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_new.rb
index cefb6775db8..016054ecd46 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_new.rb
@@ -1,8 +1,8 @@
module Gitlab
module ChatCommands
- class IssueCreate < IssueCommand
+ class IssueNew < IssueCommand
def self.match(text)
- # we can not match \n with the dot by passing the m modifier as than
+ # we can not match \n with the dot by passing the m modifier as than
# the title and description are not seperated
/\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text)
end
@@ -19,8 +19,24 @@ module Gitlab
title = match[:title]
description = match[:description].to_s.rstrip
+ issue = create_issue(title: title, description: description)
+
+ if issue.persisted?
+ presenter(issue).present
+ else
+ presenter(issue).display_errors
+ end
+ end
+
+ private
+
+ def create_issue(title:, description:)
Issues::CreateService.new(project, current_user, title: title, description: description).execute
end
+
+ def presenter(issue)
+ Gitlab::ChatCommands::Presenters::IssueNew.new(issue)
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb
index 51bf80c800b..3491b53093e 100644
--- a/lib/gitlab/chat_commands/issue_search.rb
+++ b/lib/gitlab/chat_commands/issue_search.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- collection.search(match[:query]).limit(QUERY_LIMIT)
+ issues = collection.search(match[:query]).limit(QUERY_LIMIT)
+
+ if issues.present?
+ Presenters::IssueSearch.new(issues).present
+ else
+ Presenters::Access.new(issues).not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb
index 2a45d49cf6b..d6013f4d10c 100644
--- a/lib/gitlab/chat_commands/issue_show.rb
+++ b/lib/gitlab/chat_commands/issue_show.rb
@@ -10,7 +10,13 @@ module Gitlab
end
def execute(match)
- find_by_iid(match[:iid])
+ issue = find_by_iid(match[:iid])
+
+ if issue
+ Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present
+ else
+ Gitlab::ChatCommands::Presenters::Access.new.not_found
+ end
end
end
end
diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb
deleted file mode 100644
index 8930a21f406..00000000000
--- a/lib/gitlab/chat_commands/presenter.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-module Gitlab
- module ChatCommands
- class Presenter
- include Gitlab::Routing
-
- 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(subject)
- return not_found unless subject
-
- if subject.is_a?(Gitlab::ChatCommands::Result)
- show_result(subject)
- elsif subject.respond_to?(:count)
- if subject.none?
- not_found
- elsif subject.one?
- single_resource(subject.first)
- else
- multiple_resources(subject)
- end
- else
- single_resource(subject)
- end
- end
-
- def access_denied
- ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
- end
-
- private
-
- def show_result(result)
- case result.type
- when :success
- in_channel_response(result.message)
- else
- ephemeral_response(result.message)
- end
- end
-
- 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.try(:description)
-
- in_channel_response(message)
- end
-
- def multiple_resources(resources)
- titles = resources.map { |resource| title(resource) }
-
- message = header_with_list("Multiple results were found:", titles)
-
- 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)
- reference = resource.try(:to_reference) || resource.try(:id)
- title = resource.try(:title) || resource.try(:name)
-
- "[#{reference} #{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/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb
new file mode 100644
index 00000000000..92f4fa17f78
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/access.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Access < Presenters::Base
+ def access_denied
+ ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).")
+ end
+
+ def not_found
+ ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:")
+ end
+
+ def authorize
+ message =
+ if @resource
+ ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})."
+ else
+ ":sweat_smile: Couldn't identify you, nor can I autorize you!"
+ end
+
+ ephemeral_response(text: message)
+ end
+
+ def unknown_command(commands)
+ ephemeral_response(text: help_message(trigger))
+ end
+
+ private
+
+ def help_message(trigger)
+ header_with_list("Command not found, these are the commands you can use", full_commands(trigger))
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb
new file mode 100644
index 00000000000..2700a5a2ad5
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/base.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Base
+ include Gitlab::Routing.url_helpers
+
+ def initialize(resource = nil)
+ @resource = resource
+ end
+
+ def display_errors
+ message = header_with_list("The action was not successful, because:", @resource.errors.full_messages)
+
+ ephemeral_response(text: message)
+ end
+
+ private
+
+ def header_with_list(header, items)
+ message = [header]
+
+ items.each do |item|
+ message << "- #{item}"
+ end
+
+ message.join("\n")
+ end
+
+ def ephemeral_response(message)
+ response = {
+ response_type: :ephemeral,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def in_channel_response(message)
+ response = {
+ response_type: :in_channel,
+ status: 200
+ }.merge(message)
+
+ format_response(response)
+ end
+
+ def format_response(response)
+ response[:text] = format(response[:text]) if response.has_key?(:text)
+
+ if response.has_key?(:attachments)
+ response[:attachments].each do |attachment|
+ attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext]
+ attachment[:text] = format(attachment[:text]) if attachment[:text]
+ end
+ end
+
+ response
+ end
+
+ # Convert Markdown to slacks format
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def resource_url
+ url_for(
+ [
+ @resource.project.namespace.becomes(Namespace),
+ @resource.project,
+ @resource
+ ]
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb
new file mode 100644
index 00000000000..863d0bf99ca
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/deploy.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Deploy < Presenters::Base
+ def present(from, to)
+ message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})."
+
+ in_channel_response(text: message)
+ end
+
+ def no_actions
+ ephemeral_response(text: "No action found to be executed")
+ end
+
+ def too_many_actions
+ ephemeral_response(text: "Too many actions defined")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb
new file mode 100644
index 00000000000..cd47b7f4c6a
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/help.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class Help < Presenters::Base
+ def present(trigger, text)
+ ephemeral_response(text: help_message(trigger, text))
+ end
+
+ private
+
+ def help_message(trigger, text)
+ return "No commands available :thinking_face:" unless @resource.present?
+
+ if text.start_with?('help')
+ header_with_list("Available commands", full_commands(trigger))
+ else
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ end
+ end
+
+ def full_commands(trigger)
+ @resource.map { |command| "#{trigger} #{command.help_message}" }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_base.rb b/lib/gitlab/chat_commands/presenters/issue_base.rb
new file mode 100644
index 00000000000..a0058407fb2
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_base.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ module IssueBase
+ def color(issuable)
+ issuable.open? ? '#38ae67' : '#d22852'
+ end
+
+ def status_text(issuable)
+ issuable.open? ? 'Open' : 'Closed'
+ end
+
+ def project
+ @resource.project
+ end
+
+ def author
+ @resource.author
+ end
+
+ def fields
+ [
+ {
+ title: "Assignee",
+ value: @resource.assignee ? @resource.assignee.name : "_None_",
+ short: true
+ },
+ {
+ title: "Milestone",
+ value: @resource.milestone ? @resource.milestone.title : "_None_",
+ short: true
+ },
+ {
+ title: "Labels",
+ value: @resource.labels.any? ? @resource.label_names : "_None_",
+ short: true
+ }
+ ]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb
new file mode 100644
index 00000000000..0d31660039a
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_new.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueNew < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ in_channel_response(new_issue)
+ end
+
+ private
+
+ def new_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "New issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :title,
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def pretext
+ "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}"
+ end
+
+ def project_link
+ "[#{project.name_with_namespace}](#{projects_url(project)})"
+ end
+
+ def author_profile_link
+ "[#{author.to_reference}](#{url_for(author)})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb
new file mode 100644
index 00000000000..73788cf9662
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_search.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueSearch < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ text = if @resource.count >= 5
+ "Here are the first 5 issues I found:"
+ elsif @resource.one?
+ "Here is the only issue I found:"
+ else
+ "Here are the #{@resource.count} issues I found:"
+ end
+
+ ephemeral_response(text: text, attachments: attachments)
+ end
+
+ private
+
+ def attachments
+ @resource.map do |issue|
+ url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})"
+
+ {
+ color: color(issue),
+ fallback: "#{issue.to_reference} #{issue.title}",
+ text: "#{url} · #{issue.title} (#{status_text(issue)})",
+
+ mrkdwn_in: [
+ :text
+ ]
+ }
+ end
+ end
+
+ def project
+ @project ||= @resource.first.project
+ end
+
+ def namespace
+ @namespace ||= project.namespace.becomes(Namespace)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb
new file mode 100644
index 00000000000..bd784ad241e
--- /dev/null
+++ b/lib/gitlab/chat_commands/presenters/issue_show.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module ChatCommands
+ module Presenters
+ class IssueShow < Presenters::Base
+ include Presenters::IssueBase
+
+ def present
+ if @resource.confidential?
+ ephemeral_response(show_issue)
+ else
+ in_channel_response(show_issue)
+ end
+ end
+
+ private
+
+ def show_issue
+ {
+ attachments: [
+ {
+ title: "#{@resource.title} · #{@resource.to_reference}",
+ title_link: resource_url,
+ author_name: author.name,
+ author_icon: author.avatar_url,
+ fallback: "Issue #{@resource.to_reference}: #{@resource.title}",
+ pretext: pretext,
+ text: text,
+ color: color(@resource),
+ fields: fields,
+ mrkdwn_in: [
+ :pretext,
+ :text,
+ :fields
+ ]
+ }
+ ]
+ }
+ end
+
+ def text
+ message = "**#{status_text(@resource)}**"
+
+ if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero?
+ return message
+ end
+
+ message << " · "
+ message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero?
+ message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero?
+ message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero?
+
+ message
+ end
+
+ def pretext
+ "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index 833ae4a0ff3..e05aca9881b 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -58,7 +58,7 @@ module Gitlab
def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
- @entries[symbol].specified? if @entries[symbol]
+ @entries[symbol]&.specified?
end
define_method("#{symbol}_value") do
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
new file mode 100644
index 00000000000..12a063059cb
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents Coverage settings.
+ #
+ class Coverage < Node
+ include Validatable
+
+ validations do
+ validates :config, regexp: true
+ end
+
+ def value
+ @config[1...-1]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index a55362f0b6b..69a5e6f433d 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,7 @@ module Gitlab
ALLOWED_KEYS = %i[tags script only except type image services allow_failure
type stage when artifacts cache dependencies before_script
- after_script variables environment]
+ after_script variables environment coverage]
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -71,9 +71,12 @@ module Gitlab
entry :environment, Entry::Environment,
description: 'Environment configuration for this job.'
+ entry :coverage, Entry::Coverage,
+ description: 'Coverage configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts, :commands, :environment
+ :artifacts, :commands, :environment, :coverage
attributes :script, :tags, :allow_failure, :when, :dependencies
@@ -130,6 +133,7 @@ module Gitlab
variables: variables_defined? ? variables_value : nil,
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
+ coverage: coverage_defined? ? coverage_value : nil,
artifacts: artifacts_value,
after_script: after_script_value }
end
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
index f01975aab5c..9b9a0a8125a 100644
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
@@ -28,17 +28,21 @@ module Gitlab
value.is_a?(String) || value.is_a?(Symbol)
end
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
def validate_string_or_regexp(value)
return true if value.is_a?(Symbol)
return false unless value.is_a?(String)
if value.first == '/' && value.last == '/'
- Regexp.new(value[1...-1])
+ validate_regexp(value[1...-1])
else
true
end
- rescue RegexpError
- false
end
def validate_boolean(value)
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index 28b0a9ffe01..16b234e6c59 100644
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -9,15 +9,7 @@ module Gitlab
include Validatable
validations do
- include LegacyValidationHelpers
-
- validate :array_of_strings_or_regexps
-
- def array_of_strings_or_regexps
- unless validate_array_of_strings_or_regexps(config)
- errors.add(:config, 'should be an array of strings or regexps')
- end
- end
+ validates :config, array_of_strings_or_regexps: true
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 8632dd0e233..bd7428b1272 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -54,6 +54,51 @@ module Gitlab
end
end
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+ true
+ end
+ end
+
class TypeValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
type = options[:with]
diff --git a/lib/gitlab/ci/status/build/cancelable.rb b/lib/gitlab/ci/status/build/cancelable.rb
index a979fe7d573..67bbc3c4849 100644
--- a/lib/gitlab/ci/status/build/cancelable.rb
+++ b/lib/gitlab/ci/status/build/cancelable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'ban'
+ 'icon_action_cancel'
end
def action_path
diff --git a/lib/gitlab/ci/status/build/play.rb b/lib/gitlab/ci/status/build/play.rb
index 1bf949c96dd..0f4b7b24cef 100644
--- a/lib/gitlab/ci/status/build/play.rb
+++ b/lib/gitlab/ci/status/build/play.rb
@@ -26,17 +26,13 @@ module Gitlab
end
def action_icon
- 'play'
+ 'icon_action_play'
end
def action_title
'Play'
end
- def action_class
- 'ci-play-icon'
- end
-
def action_path
play_namespace_project_build_path(subject.project.namespace,
subject.project,
diff --git a/lib/gitlab/ci/status/build/retryable.rb b/lib/gitlab/ci/status/build/retryable.rb
index 8e38d6a8523..6b362af7634 100644
--- a/lib/gitlab/ci/status/build/retryable.rb
+++ b/lib/gitlab/ci/status/build/retryable.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def action_icon
- 'refresh'
+ 'icon_action_retry'
end
def action_title
diff --git a/lib/gitlab/ci/status/build/stop.rb b/lib/gitlab/ci/status/build/stop.rb
index e1dfdb76d41..90401cad0d2 100644
--- a/lib/gitlab/ci/status/build/stop.rb
+++ b/lib/gitlab/ci/status/build/stop.rb
@@ -26,7 +26,7 @@ module Gitlab
end
def action_icon
- 'stop'
+ 'icon_action_stop'
end
def action_title
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 73b6ab5a635..3dd2b9e01f6 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -42,9 +42,6 @@ module Gitlab
raise NotImplementedError
end
- def action_class
- end
-
def action_path
raise NotImplementedError
end
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 0d8791d396b..ab115afcaa5 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -5,6 +5,8 @@ module Gitlab
attr_reader :projections, :query, :stage, :order
+ MAX_EVENTS = 50
+
def initialize(project:, stage:, options:)
@project = project
@stage = stage
@@ -38,7 +40,7 @@ module Gitlab
def events_query
diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
- base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
+ base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc).take(MAX_EVENTS)
end
def default_order
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index 74bbcdcb3dd..559e3939da6 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def as_json
- AnalyticsStageSerializer.new.represent(self).as_json
+ AnalyticsStageSerializer.new.represent(self)
end
def title
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index 5245b9ca8fc..d5bf6149749 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 0d8da99455e..3df9cbdcfce 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -16,7 +16,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
+ AnalyticsIssueSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index 88a8710dbe6..7d342a2d2cb 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -37,7 +37,7 @@ module Gitlab
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
+ AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index 4df0bd06393..4c7b3f4467f 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
+ AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index b34baf5b081..fc77bd86097 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -16,7 +16,7 @@ module Gitlab
private
def serialize(summary_object)
- AnalyticsSummarySerializer.new.represent(summary_object).as_json
+ AnalyticsSummarySerializer.new.represent(summary_object)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
index a34731a5fcd..36c0260dbfe 100644
--- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb
@@ -23,7 +23,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsBuildSerializer.new.represent(event['build']).as_json
+ AnalyticsBuildSerializer.new.represent(event['build'])
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 55b8f888d53..dc2537d36aa 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -35,6 +35,20 @@ module Gitlab
order
end
+ def self.nulls_first_order(field, direction = 'ASC')
+ order = "#{field} #{direction}"
+
+ if Gitlab::Database.postgresql?
+ order << ' NULLS FIRST'
+ else
+ # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL
+ # columns. In the (default) ascending order, `0` comes first.
+ order.prepend("#{field} IS NULL, ") if direction == 'DESC'
+ end
+
+ order
+ end
+
def self.random
Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 0bd6e148ba8..4800a509b37 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -26,11 +26,59 @@ module Gitlab
add_index(table_name, column_name, options)
end
+ # Adds a foreign key with only minimal locking on the tables involved.
+ #
+ # This method only requires minimal locking when using PostgreSQL. When
+ # using MySQL this method will use Rails' default `add_foreign_key`.
+ #
+ # source - The source table containing the foreign key.
+ # target - The target table the key points to.
+ # column - The name of the column to create the foreign key on.
+ # on_delete - The action to perform when associated data is removed,
+ # defaults to "CASCADE".
+ def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade)
+ # Transactions would result in ALTER TABLE locks being held for the
+ # duration of the transaction, defeating the purpose of this method.
+ if transaction_open?
+ raise 'add_concurrent_foreign_key can not be run inside a transaction'
+ end
+
+ # While MySQL does allow disabling of foreign keys it has no equivalent
+ # of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
+ # back to the normal foreign key procedure.
+ if Database.mysql?
+ return add_foreign_key(source, target,
+ column: column,
+ on_delete: on_delete)
+ end
+
+ disable_statement_timeout
+
+ key_name = "fk_#{source}_#{target}_#{column}"
+
+ # Using NOT VALID allows us to create a key without immediately
+ # validating it. This means we keep the ALTER TABLE lock only for a
+ # short period of time. The key _is_ enforced for any newly created
+ # data.
+ execute <<-EOF.strip_heredoc
+ ALTER TABLE #{source}
+ ADD CONSTRAINT #{key_name}
+ FOREIGN KEY (#{column})
+ REFERENCES #{target} (id)
+ ON DELETE #{on_delete} NOT VALID;
+ EOF
+
+ # Validate the existing constraint. This can potentially take a very
+ # long time to complete, but fortunately does not lock the source table
+ # while running.
+ execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
+ end
+
# Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only)
def disable_statement_timeout
- ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql?
+ execute('SET statement_timeout TO 0') if Database.postgresql?
end
# Updates the value of a column in batches.
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 127fae159d5..b8ec9138c10 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -34,7 +34,7 @@ module Gitlab
end
def project
- @project ||= Project.find_with_namespace(project_path)
+ @project ||= Project.find_by_full_path(project_path)
end
private
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 0e3b65fceb4..6c69cd9e6a9 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def diffs_count
- diffs.size if diffs
+ diffs&.size
end
def compare
@@ -58,7 +58,7 @@ module Gitlab
end
def compare_timeout
- diffs.overflow? if diffs
+ diffs&.overflow?
end
def reverse_compare?
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index a40c44eb1bc..b64db5d01ae 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -35,6 +35,8 @@ module Gitlab
handler.execute
end
+ private
+
def build_mail
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
@@ -54,7 +56,24 @@ module Gitlab
end
def key_from_additional_headers(mail)
- Array(mail.references).find do |mail_id|
+ references = ensure_references_array(mail.references)
+
+ find_key_from_references(references)
+ end
+
+ def ensure_references_array(references)
+ case references
+ when Array
+ references
+ when String
+ # Handle emails from clients which append with commas,
+ # example clients are Microsoft exchange and iOS app
+ Gitlab::IncomingEmail.scan_fallback_references(references)
+ end
+ end
+
+ def find_key_from_references(references)
+ references.find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 3cd515e4a3a..d3df3f1bca1 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -6,7 +6,7 @@ module Gitlab
class << self
def ref_name(ref)
- ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ ref.sub(/\Arefs\/(tags|heads)\//, '')
end
def branch_name(ref)
diff --git a/lib/gitlab/git/blob_snippet.rb b/lib/gitlab/git/blob_snippet.rb
index e98de57fc22..d7975f88aaa 100644
--- a/lib/gitlab/git/blob_snippet.rb
+++ b/lib/gitlab/git/blob_snippet.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def data
- lines.join("\n") if lines
+ lines&.join("\n")
end
def name
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index d32bdd86427..6babea144c7 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -30,11 +30,11 @@ module Gitlab
def retrieve_project_and_type
@type = :project
- @project = Project.find_with_namespace(@repo_path)
+ @project = Project.find_by_full_path(@repo_path)
if @repo_path.end_with?('.wiki') && !@project
@type = :wiki
- @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, ''))
+ @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, ''))
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index eb667a85b78..d679edec36b 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.5'
+ VERSION = '0.1.6'
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 08ad3274b38..416194e57d7 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -39,7 +39,6 @@ project_tree:
- :author
- :events
- :statuses
- - :variables
- :triggers
- :deploy_keys
- :services
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index 2405b94db50..8b8e48aac76 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -32,6 +32,10 @@ module Gitlab
@user.id
end
+ def include?(old_author_id)
+ map.keys.include?(old_author_id) && map[old_author_id] != default_user_id
+ end
+
private
def missing_keys_tracking_hash
@@ -41,6 +45,8 @@ module Gitlab
end
def ensure_default_member!
+ @project.project_members.destroy_all
+
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 2fbf437ec26..b79be62245b 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -5,8 +5,9 @@ module Gitlab
attr_reader :full_path
- def initialize(project:, shared:)
+ def initialize(project:, current_user:, shared:)
@project = project
+ @current_user = current_user
@shared = shared
@full_path = File.join(@shared.export_path, ImportExport.project_filename)
end
@@ -24,7 +25,29 @@ module Gitlab
private
def project_json_tree
- @project.to_json(Gitlab::ImportExport::Reader.new(shared: @shared).project_tree)
+ project_json['project_members'] += group_members_json
+
+ project_json.to_json
+ end
+
+ def project_json
+ @project_json ||= @project.as_json(reader.project_tree)
+ end
+
+ def reader
+ @reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
+ end
+
+ def group_members_json
+ group_members.as_json(reader.group_members_tree).each do |group_member|
+ group_member['source_type'] = 'Project' # Make group members project members of the future import
+ end
+ end
+
+ def group_members
+ return [] unless @current_user.can?(:admin_group, @project.group)
+
+ MembersFinder.new(@project.project_members, @project.group).execute(@current_user)
end
end
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 5021a1a14ce..a1e7159fe42 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -21,6 +21,10 @@ module Gitlab
false
end
+ def group_members_tree
+ @attributes_finder.find_included(:project_members).merge(include: @attributes_finder.find(:user))
+ end
+
private
# Builds a hash in the format described here: http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 19e43cce768..fae792237d9 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -4,7 +4,6 @@ module Gitlab
OVERRIDES = { snippets: :project_snippets,
pipelines: 'Ci::Pipeline',
statuses: 'commit_status',
- variables: 'Ci::Variable',
triggers: 'Ci::Trigger',
builds: 'Ci::Build',
hooks: 'ProjectHook',
@@ -24,6 +23,8 @@ module Gitlab
EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze
+ TOKEN_RESET_MODELS = %w[Ci::Trigger Ci::Build ProjectHook].freeze
+
def self.create(*args)
new(*args).create
end
@@ -61,7 +62,9 @@ module Gitlab
update_project_references
handle_group_label if group_label?
- reset_ci_tokens if @relation_name == 'Ci::Trigger'
+ reset_tokens!
+ remove_encrypted_attributes!
+
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diffs if @relation_name == :merge_request_diff
end
@@ -86,7 +89,7 @@ module Gitlab
end
def has_author?(old_author_id)
- admin_user? && @members_mapper.map.keys.include?(old_author_id)
+ admin_user? && @members_mapper.include?(old_author_id)
end
def missing_author_note(updated_at, author_name)
@@ -140,11 +143,22 @@ module Gitlab
end
end
- def reset_ci_tokens
- return unless Gitlab::ImportExport.reset_tokens?
+ def reset_tokens!
+ return unless Gitlab::ImportExport.reset_tokens? && TOKEN_RESET_MODELS.include?(@relation_name.to_s)
# If we import/export a project to the same instance, tokens will have to be reset.
- @relation_hash['token'] = nil
+ # We also have to reset them to avoid issues when the gitlab secrets file cannot be copied across.
+ relation_class.attribute_names.select { |name| name.include?('token') }.each do |token|
+ @relation_hash[token] = nil
+ end
+ end
+
+ def remove_encrypted_attributes!
+ return unless relation_class.respond_to?(:encrypted_attributes) && relation_class.encrypted_attributes.any?
+
+ relation_class.encrypted_attributes.each_key do |key|
+ @relation_hash[key.to_s] = nil
+ end
end
def relation_class
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index b91012d6405..c9122a23568 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -4,8 +4,6 @@ module Gitlab
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
- FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
-
def enabled?
config.enabled && config.address
end
@@ -37,10 +35,14 @@ module Gitlab
end
def key_from_fallback_message_id(mail_id)
- match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX)
- return unless match
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
- match[1]
+ mail_id[message_id_regexp, 1]
+ end
+
+ def scan_fallback_references(references)
+ # It's looking for each <...>
+ references.scan(/(?!<)[^<>]+(?=>)/)
end
def config
diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb
index 288771c1c12..3a7af363548 100644
--- a/lib/gitlab/kubernetes.rb
+++ b/lib/gitlab/kubernetes.rb
@@ -43,10 +43,10 @@ module Gitlab
end
end
- def add_terminal_auth(terminal, token, ca_pem = nil)
+ def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil)
terminal[:headers]['Authorization'] << "Bearer #{token}"
+ terminal[:max_session_time] = max_session_time
terminal[:ca_pem] = ca_pem if ca_pem.present?
- terminal
end
def container_exec_url(api_url, namespace, pod_name, container_name)
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 3d1ba33ec68..857e0abf710 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -112,7 +112,7 @@ module Gitlab
def self.tag_transaction(name, value)
trans = current_transaction
- trans.add_tag(name, value) if trans
+ trans&.add_tag(name, value)
end
# Sets the action of the current transaction (if any)
@@ -121,7 +121,7 @@ module Gitlab
def self.action=(action)
trans = current_transaction
- trans.action = action if trans
+ trans&.action = action
end
# Tracks an event.
@@ -130,7 +130,7 @@ module Gitlab
def self.add_event(*args)
trans = current_transaction
- trans.add_event(*args) if trans
+ trans&.add_event(*args)
end
# Returns the prefix to use for the name of a series.
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
new file mode 100644
index 00000000000..3fe32adeade
--- /dev/null
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -0,0 +1,24 @@
+# This Rack middleware is intended to proxy the webpack assets directory to the
+# webpack-dev-server. It is only intended for use in development.
+
+module Gitlab
+ module Middleware
+ class WebpackProxy < Rack::Proxy
+ def initialize(app = nil, opts = {})
+ @proxy_host = opts.fetch(:proxy_host, 'localhost')
+ @proxy_port = opts.fetch(:proxy_port, 3808)
+ @proxy_path = opts[:proxy_path] if opts[:proxy_path]
+ super(app, opts)
+ end
+
+ def perform_request(env)
+ unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}")
+ return @app.call(env)
+ end
+
+ env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}"
+ super(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb
new file mode 100644
index 00000000000..fb215f27cbd
--- /dev/null
+++ b/lib/gitlab/pages_transfer.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PagesTransfer < ProjectTransfer
+ def root_dir
+ Gitlab.config.pages.path
+ end
+ end
+end
diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb
new file mode 100644
index 00000000000..1bba0b78e2f
--- /dev/null
+++ b/lib/gitlab/project_transfer.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ class ProjectTransfer
+ def move_project(project_path, namespace_path_was, namespace_path)
+ new_namespace_folder = File.join(root_dir, namespace_path)
+ FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
+ from = File.join(root_dir, namespace_path_was, project_path)
+ to = File.join(root_dir, namespace_path, project_path)
+ move(from, to, "")
+ end
+
+ def rename_project(path_was, path, namespace_path)
+ base_dir = File.join(root_dir, namespace_path)
+ move(path_was, path, base_dir)
+ end
+
+ def rename_namespace(path_was, path)
+ move(path_was, path)
+ end
+
+ def root_dir
+ raise NotImplementedError
+ end
+
+ private
+
+ def move(path_was, path, base_dir = nil)
+ base_dir = root_dir unless base_dir
+ from = File.join(base_dir, path_was)
+ to = File.join(base_dir, path)
+ FileUtils.mv(from, to)
+ rescue Errno::ENOENT
+ false
+ end
+ end
+end
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 70e7f25d518..4bc76ea033f 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -10,5 +10,9 @@ module Gitlab
true
end
end
+
+ def self.enabled?
+ current_application_settings.recaptcha_enabled
+ end
end
end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 11c0b01f0dc..437a339dd2b 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,13 +1,12 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range)
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user)
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
-
@references = {}
super()
@@ -21,6 +20,11 @@ module Gitlab
super(type, project, current_user)
end
+ def reset_memoized_values
+ @references = {}
+ super()
+ end
+
REFERABLES.each do |type|
define_method("#{type}s") do
@references[type] ||= references(type)
diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
index 786e1d49f5e..ef42b0557e0 100644
--- a/lib/gitlab/request_profiler/middleware.rb
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -1,5 +1,4 @@
require 'ruby-prof'
-require_dependency 'gitlab/request_profiler'
module Gitlab
module RequestProfiler
@@ -20,7 +19,7 @@ module Gitlab
header_token = env['HTTP_X_PROFILE_TOKEN']
return unless header_token.present?
- profile_token = RequestProfiler.profile_token
+ profile_token = Gitlab::RequestProfiler.profile_token
return unless profile_token.present?
header_token == profile_token
diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb
new file mode 100644
index 00000000000..72d00abfcc2
--- /dev/null
+++ b/lib/gitlab/route_map.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class RouteMap
+ class FormatError < StandardError; end
+
+ def initialize(data)
+ begin
+ entries = YAML.safe_load(data)
+ rescue
+ raise FormatError, 'Route map is not valid YAML'
+ end
+
+ raise FormatError, 'Route map is not an array' unless entries.is_a?(Array)
+
+ @map = entries.map { |entry| parse_entry(entry) }
+ end
+
+ def public_path_for_source_path(path)
+ mapping = @map.find { |mapping| mapping[:source] === path }
+ return unless mapping
+
+ path.sub(mapping[:source], mapping[:public])
+ end
+
+ private
+
+ def parse_entry(entry)
+ raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
+ raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
+ raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
+
+ source_pattern = entry['source']
+ public_path = entry['public']
+
+ if source_pattern.start_with?('/') && source_pattern.end_with?('/')
+ source_pattern = source_pattern[1...-1].gsub('\/', '/')
+
+ begin
+ source_pattern = /\A#{source_pattern}\z/
+ rescue RegexpError => e
+ raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
+ end
+ end
+
+ {
+ source: source_pattern,
+ public: public_path
+ }
+ end
+ end
+end
diff --git a/lib/gitlab/serialize/ci/variables.rb b/lib/gitlab/serializer/ci/variables.rb
index 3a9443bfcd9..c059c454eac 100644
--- a/lib/gitlab/serialize/ci/variables.rb
+++ b/lib/gitlab/serializer/ci/variables.rb
@@ -1,5 +1,5 @@
module Gitlab
- module Serialize
+ module Serializer
module Ci
# This serializer could make sure our YAML variables' keys and values
# are always strings. This is more for legacy build data because
diff --git a/lib/gitlab/serializer/pagination.rb b/lib/gitlab/serializer/pagination.rb
new file mode 100644
index 00000000000..bf2c0acc729
--- /dev/null
+++ b/lib/gitlab/serializer/pagination.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Serializer
+ class Pagination
+ class InvalidResourceError < StandardError; end
+ include ::API::Helpers::Pagination
+
+ def initialize(request, response)
+ @request = request
+ @response = response
+ end
+
+ def paginate(resource)
+ if resource.respond_to?(:page)
+ super(resource)
+ else
+ raise InvalidResourceError
+ end
+ end
+
+ private
+
+ # Methods needed by `API::Helpers::Pagination`
+ #
+
+ attr_reader :request
+
+ def params
+ @request.query_parameters
+ end
+
+ def header(header, value)
+ @response.headers[header] = value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index be8fcc7b2d2..81701831a6a 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,33 +1,5 @@
module Gitlab
- class UploadsTransfer
- def move_project(project_path, namespace_path_was, namespace_path)
- new_namespace_folder = File.join(root_dir, namespace_path)
- FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder)
- from = File.join(root_dir, namespace_path_was, project_path)
- to = File.join(root_dir, namespace_path, project_path)
- move(from, to, "")
- end
-
- def rename_project(path_was, path, namespace_path)
- base_dir = File.join(root_dir, namespace_path)
- move(path_was, path, base_dir)
- end
-
- def rename_namespace(path_was, path)
- move(path_was, path)
- end
-
- private
-
- def move(path_was, path, base_dir = nil)
- base_dir = root_dir unless base_dir
- from = File.join(base_dir, path_was)
- to = File.join(base_dir, path)
- FileUtils.mv(from, to)
- rescue Errno::ENOENT
- false
- end
-
+ class UploadsTransfer < ProjectTransfer
def root_dir
File.join(Rails.root, "public", "uploads")
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index c7953af29dd..a4e966e4016 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -13,7 +13,19 @@ module Gitlab
scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) }
scope :non_public_only, -> { where.not(visibility_level: PUBLIC) }
- scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only }
+ scope :public_to_user, -> (user) do
+ if user
+ if user.admin?
+ all
+ elsif !user.external?
+ public_and_internal_only
+ else
+ public_only
+ end
+ else
+ public_only
+ end
+ end
end
PRIVATE = 0 unless const_defined?(:PRIVATE)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index a3b502ffd6a..c8872df8a93 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -107,7 +107,8 @@ module Gitlab
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
- 'Header' => terminal[:headers]
+ 'Header' => terminal[:headers],
+ 'MaxSessionTime' => terminal[:max_session_time],
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem)
diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb
new file mode 100644
index 00000000000..7d5700b7f6d
--- /dev/null
+++ b/lib/rouge/lexers/plantuml.rb
@@ -0,0 +1,21 @@
+module Rouge
+ module Lexers
+ class Plantuml < Lexer
+ title "A passthrough lexer used for PlantUML input"
+ desc "A boring lexer that doesn't highlight anything"
+
+ tag 'plantuml'
+ mimetypes 'text/plain'
+
+ default_options token: 'Text'
+
+ def token
+ @token ||= Token[option :token]
+ end
+
+ def stream_tokens(string, &b)
+ yield self.token, string
+ end
+ end
+ end
+end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 31b00ff128a..5fd7f0f98bd 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+gitlab_pages_enabled=false
+gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd)
+gitlab_pages_pid_path="$pid_path/gitlab-pages.pid"
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -89,13 +94,20 @@ check_pids(){
mpid=0
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ -f "$gitlab_pages_pid_path" ]; then
+ gppid=$(cat "$gitlab_pages_pid_path")
+ else
+ gppid=0
+ fi
+ fi
}
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
# We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; } || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -144,7 +156,15 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ $gppid -ne 0 ]; then
+ kill -0 "$gppid" 2>/dev/null
+ gitlab_pages_status="$?"
+ else
+ gitlab_pages_status="-1"
+ fi
+ fi
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -186,12 +206,19 @@ check_stale_pids(){
exit 1
fi
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran."
+ if ! rm "$gitlab_pages_pid_path"; then
+ echo "Unable to remove stale pid, exiting"
+ exit 1
+ fi
+ fi
}
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -213,6 +240,9 @@ start_gitlab() {
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
fi
+ if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then
+ echo "Starting GitLab Pages"
+ fi
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
@@ -252,6 +282,16 @@ start_gitlab() {
fi
fi
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages is already running with pid $spid, not restarting"
+ else
+ $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \
+ $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \
+ >> $gitlab_pages_log 2>&1 &
+ fi
+ fi
+
# Wait for the pids to be planted
wait_for_pids
# Finally check the status to tell wether or not GitLab is running
@@ -278,13 +318,17 @@ stop_gitlab() {
echo "Shutting down GitLab MailRoom"
RAILS_ENV=$RAILS_ENV bin/mail_room stop
fi
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "Shutting down gitlab-pages"
+ kill -- $(cat $gitlab_pages_pid_path)
+ fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -298,6 +342,7 @@ stop_gitlab() {
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
+ rm -f "$gitlab_pages_pid_path"
print_status
}
@@ -305,7 +350,7 @@ stop_gitlab() {
## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -331,7 +376,14 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$gitlab_pages_enabled" = true ]; then
+ if [ "$gitlab_pages_status" = "0" ]; then
+ echo "The GitLab Pages with pid $mpid is running."
+ else
+ printf "The GitLab Pages is \033[31mnot running\033[0m.\n"
+ fi
+ fi
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
@@ -362,7 +414,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index cc8617b72ca..e5797d8fe3c 100755..100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
+# The GitLab Pages Daemon needs either a separate IP address on which it will
+# listen or use different ports than 80 or 443 that will be forwarded to GitLab
+# Pages Daemon.
+#
+# To enable HTTP support for custom domains add the `-listen-http` directive
+# in `gitlab_pages_options` below.
+# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
+# as well. For example:
+#
+# -listen-http 1.1.1.1:80
+#
+# To enable HTTPS support for custom domains add the `-listen-https`,
+# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
+# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
+# as well. For example:
+#
+# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+#
+# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
+# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
+gitlab_pages_enabled=false
+gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090"
+gitlab_pages_log="$app_root/log/gitlab-pages.log"
+
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
# The default is "false"
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
new file mode 100644
index 00000000000..d9746c5c1aa
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages
@@ -0,0 +1,28 @@
+## GitLab
+##
+
+## Pages serving host
+server {
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
new file mode 100644
index 00000000000..a1ccf266835
--- /dev/null
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -0,0 +1,77 @@
+## GitLab
+##
+
+## Redirects all HTTP traffic to the HTTPS host
+server {
+ ## Either remove "default_server" from the listen line below,
+ ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
+ ## to be served if you visit any address that your server responds to, eg.
+ ## the ip address of the server (http://x.x.x.x/)
+ listen 0.0.0.0:80;
+ listen [::]:80 ipv6only=on;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ return 301 https://$http_host$request_uri;
+
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_access.log;
+}
+
+## Pages serving host
+server {
+ listen 0.0.0.0:443 ssl;
+ listen [::]:443 ipv6only=on ssl http2;
+
+ ## Replace this with something like pages.gitlab.com
+ server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$;
+ server_tokens off; ## Don't show the nginx version number, a security best practice
+
+ ## Strong SSL Security
+ ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/gitlab-pages.crt;
+ ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key;
+
+ # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs
+ ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 5m;
+
+ ## See app/controllers/application_controller.rb for headers set
+
+ ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL.
+ ## Replace with your ssl_trusted_certificate. For more info see:
+ ## - https://medium.com/devops-programming/4445f4862461
+ ## - https://www.ruby-forum.com/topic/4419319
+ ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx
+ # ssl_stapling on;
+ # ssl_stapling_verify on;
+ # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt;
+
+ ## [Optional] Generate a stronger DHE parameter:
+ ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
+ ##
+ # ssl_dhparam /etc/ssl/certs/dhparam.pem;
+
+ ## Individual nginx logs for GitLab pages
+ access_log /var/log/nginx/gitlab_pages_access.log;
+ error_log /var/log/nginx/gitlab_pages_error.log;
+
+ location / {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ # The same address as passed to GitLab Pages: `-listen-proxy`
+ proxy_pass http://localhost:8090/;
+ }
+
+ # Define custom error pages
+ error_page 403 /403.html;
+ error_page 404 /404.html;
+}
diff --git a/lib/tasks/config_lint.rake b/lib/tasks/config_lint.rake
new file mode 100644
index 00000000000..ddbcf1e1eb8
--- /dev/null
+++ b/lib/tasks/config_lint.rake
@@ -0,0 +1,25 @@
+module ConfigLint
+ def self.run(files)
+ failures = files.reject do |file|
+ yield(file)
+ end
+
+ if failures.present?
+ puts failures
+ exit failures.count
+ end
+ end
+end
+
+desc "Checks syntax for shell scripts and nginx config files in 'lib/support/'"
+task :config_lint do
+ shell_scripts = [
+ 'lib/support/init.d/gitlab',
+ 'lib/support/init.d/gitlab.default.example',
+ 'lib/support/deploy/deploy.sh'
+ ]
+
+ ConfigLint.run(shell_scripts) do |file|
+ Kernel.system('bash', '-n', file)
+ end
+end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 5d884bf9f66..b6ef8260191 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -3,6 +3,7 @@ namespace :gitlab do
desc 'GitLab | Assets | Compile all frontend assets'
task :compile do
Rake::Task['assets:precompile'].invoke
+ Rake::Task['webpack:compile'].invoke
Rake::Task['gitlab:assets:fix_urls'].invoke
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index a9f1255e8cf..1650263b98d 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:pages:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
@@ -56,6 +57,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+ Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?('pages')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke
@@ -159,6 +161,25 @@ namespace :gitlab do
end
end
+ namespace :pages do
+ task create: :environment do
+ $progress.puts "Dumping pages ... ".color(:blue)
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("pages")
+ $progress.puts "[SKIPPED]".color(:cyan)
+ else
+ Backup::Pages.new.dump
+ $progress.puts "done".color(:green)
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring pages ... ".color(:blue)
+ Backup::Pages.new.restore
+ $progress.puts "done".color(:green)
+ end
+ end
+
namespace :lfs do
task create: :environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 4a696a52b4d..967f630ef20 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -58,7 +58,7 @@ namespace :gitlab do
sub(%r{^/*}, '').
chomp('.git').
chomp('.wiki')
- next if Project.find_with_namespace(repo_with_namespace)
+ next if Project.find_by_full_path(repo_with_namespace)
new_path = path + move_suffix
puts path.inspect + ' -> ' + new_path.inspect
File.rename(path, new_path)
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index a2eca74a3c8..b4015f5238e 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -29,7 +29,7 @@ namespace :gitlab do
next
end
- project = Project.find_with_namespace(path)
+ project = Project.find_by_full_path(path)
if project
puts " * #{project.name} (#{repo_path}) exists"
@@ -63,7 +63,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
- ProjectCacheWorker.perform(project.id)
+ ProjectCacheWorker.perform_async(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red)
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index 7e2a6668e59..f2e12d85045 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -7,7 +7,7 @@ namespace :gitlab do
unless args.project.present?
abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]"
end
- project_path = Project.find_with_namespace(args.project).repository.path_to_repo
+ project_path = Project.find_by_full_path(args.project).repository.path_to_repo
Sidekiq.redis do |redis|
unless redis.exists(QUEUE)
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index 4d4e746503a..84810b489ce 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
%W(rake rubocop),
%W(rake spinach),
%W(rake spec),
- %W(rake teaspoon)
+ %W(rake karma)
]
cmds.each do |cmd|
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
index 9980e0b7984..ea2698da606 100644
--- a/lib/tasks/grape.rake
+++ b/lib/tasks/grape.rake
@@ -2,7 +2,11 @@ namespace :grape do
desc 'Print compiled grape routes'
task routes: :environment do
API::API.routes.each do |route|
- puts route
+ puts "#{route.options[:method]} #{route.path} - #{route_description(route.options)}"
end
end
+
+ def route_description(options)
+ options[:settings][:description][:description] if options[:settings][:description]
+ end
end
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
new file mode 100644
index 00000000000..89812a179ec
--- /dev/null
+++ b/lib/tasks/karma.rake
@@ -0,0 +1,25 @@
+unless Rails.env.production?
+ Rake::Task['karma'].clear if Rake::Task.task_defined?('karma')
+
+ namespace :karma do
+ desc 'GitLab | Karma | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures) do |t|
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.rspec_opts = '--format documentation'
+ end
+
+ desc 'GitLab | Karma | Run JavaScript tests'
+ task :tests do
+ sh "npm run karma" do |ok, res|
+ abort('rake karma:tests failed') unless ok
+ end
+ end
+ end
+
+ desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests'
+ task :karma do
+ Rake::Task['karma:fixtures'].invoke
+ Rake::Task['karma:tests'].invoke
+ end
+end
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
deleted file mode 100644
index 08caedd7ff3..00000000000
--- a/lib/tasks/teaspoon.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-unless Rails.env.production?
- Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
-
- namespace :teaspoon do
- desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
- ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
- t.rspec_opts = '--format documentation'
- end
-
- desc 'GitLab | Teaspoon | Run JavaScript tests'
- task :tests do
- require "teaspoon/console"
- options = {}
- abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
- end
- end
-
- desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
- task :teaspoon do
- Rake::Task['teaspoon:fixtures'].invoke
- Rake::Task['teaspoon:tests'].invoke
- end
-end
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index d3dcbd2c29b..3e01f91d32c 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -7,5 +7,5 @@ end
unless Rails.env.production?
desc "GitLab | Run all tests on CI with simplecov"
- task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec]
+ task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec]
end
diff --git a/package.json b/package.json
index 49b8210e427..24e11a4607f 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,39 @@
{
"private": true,
"scripts": {
+ "dev-server": "webpack-dev-server --config config/webpack.config.js",
"eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
"eslint-fix": "npm run eslint -- --fix",
- "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html"
+ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html",
+ "karma": "karma start config/karma.config.js --single-run",
+ "karma-start": "karma start config/karma.config.js",
+ "webpack": "webpack --config config/webpack.config.js",
+ "webpack-prod": "NODE_ENV=production npm run webpack"
+ },
+ "dependencies": {
+ "babel-core": "^6.22.1",
+ "babel-loader": "^6.2.10",
+ "babel-preset-es2015": "^6.22.0",
+ "babel-preset-stage-2": "^6.22.0",
+ "bootstrap-sass": "3.3.6",
+ "compression-webpack-plugin": "^0.3.2",
+ "d3": "3.5.11",
+ "dropzone": "4.2.0",
+ "imports-loader": "^0.6.5",
+ "jquery": "2.2.1",
+ "jquery-ui": "github:jquery/jquery-ui#1.11.4",
+ "jquery-ujs": "1.2.1",
+ "js-cookie": "^2.1.3",
+ "mousetrap": "1.4.6",
+ "pikaday": "^1.5.1",
+ "select2": "3.5.2-browserify",
+ "stats-webpack-plugin": "^0.4.3",
+ "timeago.js": "^2.0.5",
+ "underscore": "1.8.3",
+ "vue": "2.0.3",
+ "vue-resource": "0.9.3",
+ "webpack": "^2.2.1",
+ "webpack-dev-server": "^2.3.0"
},
"devDependencies": {
"eslint": "^3.10.1",
@@ -11,6 +41,13 @@
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
- "istanbul": "^0.4.5"
+ "istanbul": "^0.4.5",
+ "jasmine-core": "^2.5.2",
+ "jasmine-jquery": "^2.1.1",
+ "karma": "^1.4.1",
+ "karma-jasmine": "^1.1.0",
+ "karma-phantomjs-launcher": "^1.0.2",
+ "karma-sourcemap-loader": "^0.3.7",
+ "karma-webpack": "^2.0.2"
}
}
diff --git a/rubocop/cop/gem_fetcher.rb b/rubocop/cop/gem_fetcher.rb
new file mode 100644
index 00000000000..c199f6acab2
--- /dev/null
+++ b/rubocop/cop/gem_fetcher.rb
@@ -0,0 +1,37 @@
+module RuboCop
+ module Cop
+ # This cop prevents usage of the `git` and `github` arguments to `gem` in a
+ # `Gemfile` in order to avoid additional points of failure beyond
+ # rubygems.org.
+ class GemFetcher < RuboCop::Cop::Cop
+ MSG = 'Do not use gems from git repositories, only use gems from RubyGems.'
+
+ GIT_KEYS = [:git, :github]
+
+ def on_send(node)
+ return unless gemfile?(node)
+
+ func_name = node.children[1]
+ return unless func_name == :gem
+
+ node.children.last.each_node(:pair) do |pair|
+ key_name = pair.children[0].children[0].to_sym
+ if GIT_KEYS.include?(key_name)
+ add_offense(node, pair.source_range, MSG)
+ end
+ end
+ end
+
+ private
+
+ def gemfile?(node)
+ node
+ .location
+ .expression
+ .source_buffer
+ .name
+ .end_with?("Gemfile")
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/column_with_default.rb b/rubocop/cop/migration/add_column.rb
index 97ee8b11044..1490fcdd814 100644
--- a/rubocop/cop/migration/column_with_default.rb
+++ b/rubocop/cop/migration/add_column.rb
@@ -1,15 +1,17 @@
+require_relative '../../migration_helpers'
+
module RuboCop
module Cop
module Migration
# Cop that checks if columns are added in a way that doesn't require
# downtime.
- class ColumnWithDefault < RuboCop::Cop::Cop
+ class AddColumn < RuboCop::Cop::Cop
include MigrationHelpers
WHITELISTED_TABLES = [:application_settings]
- MSG = 'add_column with a default value requires downtime, ' \
- 'use add_column_with_default instead'
+ MSG = '`add_column` with a default value requires downtime, ' \
+ 'use `add_column_with_default` instead'
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
new file mode 100644
index 00000000000..747d7caf1ef
--- /dev/null
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -0,0 +1,34 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_column_with_default` is used with `up`/`down` methods
+ # and not `change`.
+ class AddColumnWithDefault < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`add_column_with_default` is not reversible so you must manually define ' \
+ 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_column_with_default
+
+ node.each_ancestor(:def) do |def_node|
+ next unless method_name(def_node) == :change
+
+ add_offense(def_node, :name)
+ end
+ end
+
+ def method_name(node)
+ node.children.first
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
new file mode 100644
index 00000000000..e40a7087a47
--- /dev/null
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -0,0 +1,27 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_concurrent_foreign_key` is used instead of
+ # `add_foreign_key`.
+ class AddConcurrentForeignKey < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ add_offense(node, :selector) if name == :add_foreign_key
+ end
+
+ def method_name(node)
+ node.children.first
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/add_index.rb b/rubocop/cop/migration/add_index.rb
index d9247a1f7ea..5e6766f6994 100644
--- a/rubocop/cop/migration/add_index.rb
+++ b/rubocop/cop/migration/add_index.rb
@@ -1,3 +1,5 @@
+require_relative '../../migration_helpers'
+
module RuboCop
module Cop
module Migration
@@ -5,7 +7,7 @@ module RuboCop
class AddIndex < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = 'add_index requires downtime, use add_concurrent_index instead'
+ MSG = '`add_index` requires downtime, use `add_concurrent_index` instead'
def on_def(node)
return unless in_migration?(node)
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 7922e19768b..d4266d0deae 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,3 +1,5 @@
-require_relative 'migration_helpers'
+require_relative 'cop/gem_fetcher'
+require_relative 'cop/migration/add_column'
+require_relative 'cop/migration/add_column_with_default'
+require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_index'
-require_relative 'cop/migration/column_with_default'
diff --git a/shared/pages/.gitkeep b/shared/pages/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/pages/.gitkeep
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
new file mode 100644
index 00000000000..566d8515198
--- /dev/null
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe DashboardController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET issues' do
+ it_behaves_like 'issuables list meta-data', :issue, :issues
+ end
+
+ describe 'GET merge requests' do
+ it_behaves_like 'issuables list meta-data', :merge_request, :merge_requests
+ end
+end
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 299d2c981d3..ad15e3942a5 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do
end
describe 'GET index' do
- context 'with valid list id' do
- it 'returns issues that have the list label applied' do
- johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
- issue = create(:labeled_issue, project: project, labels: [planning])
- 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, project)
-
- list_issues user: user, board: board, list: list2
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('issues')
- expect(parsed_response.length).to eq 2
- end
- end
+ let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
context 'with invalid board id' do
it 'returns a not found 404 response' do
@@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do
end
end
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- list_issues user: user, board: board, list: 999
+ context 'when list id is present' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ issue = create(:labeled_issue, project: project, labels: [planning])
+ 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, project)
- expect(response).to have_http_status(404)
+ list_issues user: user, board: board, list: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: board, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when list id is missing' do
+ it 'returns opened issues without board labels applied' do
+ bug = create(:label, project: project, name: 'Bug')
+ create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [bug])
+
+ list_issues user: user, board: board
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
end
end
@@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do
end
end
- def list_issues(user:, board:, list:)
+ def list_issues(user:, board:, list: nil)
sign_in(user)
- get :index, namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- board_id: board.to_param,
- list_id: list.to_param
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ board_id: board.to_param,
+ list_id: list.try(:to_param)
+ }
+
+ get :index, params.compact
end
end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 34d6119429d..b3f9f76a50c 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('lists')
- expect(parsed_response.length).to eq 3
+ expect(parsed_response.length).to eq 2
end
context 'with unauthorized user' do
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index a95cfc5c6be..ebd2d0e092b 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -4,7 +4,6 @@ describe Projects::CommitController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:commit) { project.commit("master") }
- let(:pipeline) { create(:ci_pipeline, project: project, commit: commit) }
let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let(:master_pickable_commit) { project.commit(master_pickable_sha) }
@@ -322,11 +321,26 @@ describe Projects::CommitController do
end
context 'when the commit exists' do
- context 'when the commit has one or more pipelines' do
- it 'shows pipelines' do
- get_pipelines(id: commit.id)
+ context 'when the commit has pipelines' do
+ before do
+ create(:ci_pipeline, project: project, sha: commit.id)
+ end
+
+ context 'when rendering a HTML format' do
+ it 'shows pipelines' do
+ get_pipelines(id: commit.id)
+
+ expect(response).to be_ok
+ end
+ end
- expect(response).to be_ok
+ context 'when rendering a JSON format' do
+ it 'responds with serialized pipelines' do
+ get_pipelines(id: commit.id, format: :json)
+
+ expect(response).to be_ok
+ expect(JSON.parse(response.body)).not_to be_empty
+ end
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5f27f336f72..e576bf9ef79 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -24,6 +24,8 @@ describe Projects::IssuesController do
project.team << [user, :developer]
end
+ it_behaves_like "issuables list meta-data", :issue
+
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
@@ -326,7 +328,7 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
- def post_new_issue(attrs = {})
+ def post_new_issue(issue_attrs = {}, additional_params = {})
sign_in(user)
project = create(:empty_project, :public)
project.team << [user, :developer]
@@ -334,8 +336,8 @@ describe Projects::IssuesController do
post :create, {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
- issue: { title: 'Title', description: 'Description' }.merge(attrs)
- }
+ issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
+ }.merge(additional_params)
project.issues.first
end
@@ -378,24 +380,81 @@ describe Projects::IssuesController do
context 'Akismet is enabled' do
before do
+ stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
- def post_spam_issue
- post_new_issue(title: 'Spam Title', description: 'Spam lives here')
- end
+ context 'when an issue is not identified as a spam' do
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
- it 'rejects an issue recognized as spam' do
- expect{ post_spam_issue }.not_to change(Issue, :count)
- expect(response).to render_template(:new)
+ it 'does not create an issue' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
end
- it 'creates a spam log' do
- post_spam_issue
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('Spam Title')
+ context 'when an issue is identified as a spam' do
+ before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
+
+ context 'when captcha is not verified' do
+ def post_spam_issue
+ post_new_issue(title: 'Spam Title', description: 'Spam lives here')
+ end
+
+ before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
+
+ it 'rejects an issue recognized as a spam' do
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+
+ it 'creates a spam log' do
+ post_spam_issue
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs.first.title).to eq('Spam Title')
+ expect(spam_logs.first.recaptcha_verified).to be_falsey
+ end
+
+ it 'does not create an issue when it is not valid' do
+ expect { post_new_issue(title: '') }.not_to change(Issue, :count)
+ end
+
+ it 'does not create an issue when recaptcha is not enabled' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect { post_spam_issue }.not_to change(Issue, :count)
+ end
+ end
+
+ context 'when captcha is verified' do
+ let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
+
+ def post_verified_issue
+ post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
+ end
+
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
+ end
+
+ it 'accepts an issue after recaptcha is verified' do
+ expect { post_verified_issue }.to change(Issue, :count)
+ end
+
+ it 'marks spam log as recaptcha_verified' do
+ expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ end
+
+ it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
+ spam_log = create(:spam_log)
+
+ expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }.
+ not_to change { SpamLog.last.recaptcha_verified }
+ end
+ end
end
end
@@ -405,7 +464,7 @@ describe Projects::IssuesController do
end
it 'creates a user agent detail' do
- expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
end
end
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index ec6cea5c0f4..3e0326dd47d 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -112,4 +112,49 @@ describe Projects::LabelsController do
post :toggle_subscription, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label.to_param
end
end
+
+ describe 'POST #promote' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:label_1) { create(:label, title: promoted_label_name, project: project) }
+
+ context 'not group owner' do
+ it 'denies access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'group owner' do
+ before do
+ GroupMember.add_users_to_group(group, [user], :owner)
+ end
+
+ it 'gives access' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+
+ it 'promotes the label' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+
+ expect(Label.where(id: label_1.id)).to be_empty
+ expect(GroupLabel.find_by(title: promoted_label_name)).not_to be_nil
+ end
+
+ context 'service raising InvalidRecord' do
+ before do
+ expect_any_instance_of(Labels::PromoteService).to receive(:execute) do |label|
+ raise ActiveRecord::RecordInvalid.new(label_1)
+ end
+ end
+
+ it 'returns to label list' do
+ post :promote, namespace_id: project.namespace.to_param, project_id: project.to_param, id: label_1.to_param
+ expect(response).to redirect_to(namespace_project_labels_path)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 2ae635a1244..cae733f0cfb 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -13,13 +13,13 @@ describe Projects::MattermostsController do
before do
allow_any_instance_of(MattermostSlashCommandsService).
to receive(:list_teams).and_return([])
+ end
+ it 'accepts the request' do
get(:new,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
- end
- it 'accepts the request' do
expect(response).to have_http_status(200)
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 7ea3ea4f376..bfd134e406e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::MergeRequestsController do
+ include ApiHelpers
+
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -20,23 +22,41 @@ describe Projects::MergeRequestsController do
render_views
let(:fork_project) { create(:forked_project_with_submodules) }
+ before { fork_project.team << [user, :master] }
- before do
- fork_project.team << [user, :master]
+ context 'when rendering HTML response' do
+ it 'renders new merge request widget template' do
+ submit_new_merge_request
+
+ expect(response).to be_success
+ end
end
- it 'renders it' do
- get :new,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
- merge_request: {
- source_branch: 'remove-submodule',
- target_branch: 'master'
- }
+ context 'when rendering JSON response' do
+ before do
+ create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
+ ref: 'remove-submodule',
+ project: fork_project)
+ end
- expect(response).to be_success
+ it 'renders JSON including serialized pipelines' do
+ submit_new_merge_request(format: :json)
+
+ expect(response).to be_ok
+ expect(json_response).not_to be_empty
+ end
end
end
+
+ def submit_new_merge_request(format: :html)
+ get :new,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ merge_request: {
+ source_branch: 'remove-submodule',
+ target_branch: 'master' },
+ format: format
+ end
end
shared_examples "loads labels" do |action|
@@ -127,6 +147,8 @@ describe Projects::MergeRequestsController do
end
describe 'GET index' do
+ let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+
def get_merge_requests(page = nil)
get :index,
namespace_id: project.namespace.to_param,
@@ -134,6 +156,8 @@ describe Projects::MergeRequestsController do
state: 'opened', page: page.to_param
end
+ it_behaves_like "issuables list meta-data", :merge_request
+
context 'when page param' do
let(:last_page) { project.merge_requests.page().total_pages }
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -455,7 +479,7 @@ describe Projects::MergeRequestsController do
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
@@ -494,7 +518,7 @@ describe Projects::MergeRequestsController do
it 'renders the diffs template to a string' do
expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
end
@@ -662,18 +686,38 @@ describe Projects::MergeRequestsController do
go format: 'json'
expect(response).to render_template('projects/merge_requests/show/_commits')
- expect(JSON.parse(response.body)).to have_key('html')
+ expect(json_response).to have_key('html')
end
end
end
describe 'GET pipelines' do
- it_behaves_like "loads labels", :pipelines
+ before do
+ create(:ci_pipeline, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ context 'when using HTML format' do
+ it_behaves_like "loads labels", :pipelines
+ end
+
+ context 'when using JSON format' do
+ before do
+ get :pipelines,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: :json
+ end
+
+ it 'responds with serialized pipelines' do
+ expect(json_response).not_to be_empty
+ end
+ end
end
describe 'GET conflicts' do
- let(:json_response) { JSON.parse(response.body) }
-
context 'when the conflicts cannot be resolved in the UI' do
before do
allow_any_instance_of(Gitlab::Conflict::Parser).
@@ -770,8 +814,6 @@ describe Projects::MergeRequestsController do
end
describe 'GET conflict_for_path' do
- let(:json_response) { JSON.parse(response.body) }
-
def conflict_for_path(path)
get :conflict_for_path,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
@@ -826,7 +868,6 @@ describe Projects::MergeRequestsController do
end
context 'POST resolve_conflicts' do
- let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
def resolve_conflicts(files)
@@ -1024,7 +1065,6 @@ describe Projects::MergeRequestsController do
let!(:forked) { create(:project) }
let!(:environment) { create(:environment, project: forked) }
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
- let(:json_response) { JSON.parse(response.body) }
let(:admin) { create(:admin) }
let(:merge_request) do
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
new file mode 100644
index 00000000000..2362df895a8
--- /dev/null
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Projects::PagesDomainsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project
+ }
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'GET show' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "displays the 'show' page" do
+ get(:show, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'GET new' do
+ it "displays the 'new' page" do
+ get(:new, request_params)
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('new')
+ end
+ end
+
+ describe 'POST create' do
+ let(:pages_domain_params) do
+ build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain)
+ end
+
+ it "creates a new pages domain" do
+ expect do
+ post(:create, request_params.merge(pages_domain: pages_domain_params))
+ end.to change { PagesDomain.count }.by(1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:pages_domain) { create(:pages_domain, project: project) }
+
+ it "deletes the pages domain" do
+ expect do
+ delete(:destroy, request_params.merge(id: pages_domain.domain))
+ end.to change { PagesDomain.count }.by(-1)
+
+ expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project))
+ end
+ end
+end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
new file mode 100644
index 00000000000..e9a91cff1b3
--- /dev/null
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -0,0 +1,20 @@
+require('spec_helper')
+
+describe Projects::Settings::CiCdController do
+ let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 32b0e42c3cd..19e948d8fb8 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -6,8 +6,8 @@ describe Projects::SnippetsController do
let(:user2) { create(:user) }
before do
- project.team << [user, :master]
- project.team << [user2, :master]
+ project.add_master(user)
+ project.add_master(user2)
end
describe 'GET #index' do
@@ -69,6 +69,86 @@ describe Projects::SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(project, snippet_params = {})
+ sign_in(user)
+
+ project.add_developer(user)
+
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ project.add_master(admin)
+ sign_in(admin)
+
+ post :mark_as_spam,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w[show raw].each do |action|
describe "GET ##{action}" do
context 'when the project snippet is private' do
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 99d0bcfa8d1..80f84a388ce 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -14,7 +14,8 @@ describe Projects::TemplatesController do
before do
project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_1, 'something valid',
+ message: 'test 3', branch_name: 'master', update: false)
end
describe '#show' do
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index f1c8891e87a..0347e789576 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -170,68 +170,24 @@ describe Projects::UploadsController do
project.team << [user, :master]
end
- context "when the user is blocked" do
+ context "when the file exists" do
before do
- user.block
- project.team << [user, :master]
- end
-
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- context "when the file is an image" do
- before do
- allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
end
- context "when the file doesn't exist" do
- it "redirects to the sign in page" do
- go
+ it "responds with status 200" do
+ go
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to have_http_status(200)
end
end
- context "when the user isn't blocked" do
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
new file mode 100644
index 00000000000..9fa358f7d62
--- /dev/null
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Projects::VariablesController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ describe 'POST #create' do
+ context 'variable is valid' do
+ it 'shows a success flash message' do
+ post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ variable: { key: "one", value: "two" }
+
+ expect(flash[:notice]).to include 'Variables were successfully updated.'
+ expect(response).to redirect_to(namespace_project_settings_ci_cd_path(project.namespace, project))
+ end
+ end
+
+ context 'variable is invalid' do
+ it 'shows an alert flash message' do
+ post :create, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ variable: { key: "..one", value: "two" }
+
+ expect(response).to render_template("projects/variables/show")
+ end
+ end
+ end
+
+ describe 'POST #update' do
+ let(:variable) { create(:ci_variable) }
+
+ context 'updating a variable with valid characters' do
+ before do
+ variable.gl_project_id = project.id
+ project.variables << variable
+ end
+
+ it 'shows a success flash message' do
+ post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ id: variable.id, variable: { key: variable.key, value: 'two' }
+
+ expect(flash[:notice]).to include 'Variable was successfully updated.'
+ expect(response).to redirect_to(namespace_project_variables_path(project.namespace, project))
+ end
+
+ it 'renders the action #show if the variable key is invalid' do
+ post :update, namespace_id: project.namespace.to_param, project_id: project.to_param,
+ id: variable.id, variable: { key: '?', value: variable.value }
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template :show
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 9323f723bdb..e7aa8745b99 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -213,6 +213,17 @@ describe ProjectsController do
expect(response.status).to eq 404
end
end
+
+ context "redirection from http://someproject.git" do
+ it 'redirects to project page (format.html)' do
+ project = create(:project, :public)
+
+ get :show, namespace_id: project.namespace.path, id: project.path, format: :git
+
+ expect(response).to have_http_status(302)
+ expect(response).to redirect_to(namespace_project_path)
+ end
+ end
end
describe "#update" do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 42fbfe89368..8cc216445eb 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -44,7 +44,7 @@ describe RegistrationsController do
post(:create, user_params)
expect(response).to render_template(:new)
- expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
+ expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
it 'redirects to the dashboard when the recaptcha is solved' do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index b7bb9290712..3173aae664c 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe SearchController do
let(:user) { create(:user) }
- let(:project) { create(:empty_project, :public) }
before do
sign_in(user)
@@ -22,7 +21,7 @@ describe SearchController do
before { sign_out(user) }
it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -31,17 +30,8 @@ describe SearchController do
end
end
- it "doesn't expose comments on issues" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
- note = create(:note_on_issue, project: project)
-
- get :show, project_id: project.id, scope: 'notes', search: note.note
-
- expect(assigns[:search_objects].count).to eq(0)
- end
-
it "doesn't expose comments on merge_requests" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
@@ -50,7 +40,7 @@ describe SearchController do
end
it "doesn't expose comments on snippets" do
- project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :snippets_private)
note = create(:note_on_project_snippet, project: project)
get :show, project_id: project.id, scope: 'notes', search: note.note
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index d76fe9f580f..dadcb90cfc2 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -138,6 +138,65 @@ describe SnippetsController do
end
end
+ describe 'POST #create' do
+ def create_snippet(snippet_params = {})
+ sign_in(user)
+
+ post :create, {
+ personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params)
+ }
+ end
+
+ context 'when the snippet is spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ let(:snippet) { create(:personal_snippet, :public, author: user) }
+
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ stub_application_setting(akismet_enabled: true)
+ end
+
+ def mark_as_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: snippet)
+ sign_in(admin)
+
+ post :mark_as_spam, id: snippet.id
+ end
+
+ it 'updates the snippet' do
+ mark_as_spam
+
+ expect(snippet.reload).not_to be_submittable_as_spam
+ end
+ end
+
%w(raw download).each do |action|
describe "GET #{action}" do
context 'when the personal snippet is private' do
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index ec46146d9b5..a581725245a 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -3,7 +3,6 @@ FactoryGirl.define do
project factory: :empty_project
after(:create) do |board|
- board.lists.create(list_type: :backlog)
board.lists.create(list_type: :done)
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index bfe41f71b57..55727d6b62c 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -3,6 +3,18 @@ FactoryGirl.define do
project factory: :empty_project
author factory: :user
+ trait(:created) { action Event::CREATED }
+ trait(:updated) { action Event::UPDATED }
+ trait(:closed) { action Event::CLOSED }
+ trait(:reopened) { action Event::REOPENED }
+ trait(:pushed) { action Event::PUSHED }
+ trait(:commented) { action Event::COMMENTED }
+ trait(:merged) { action Event::MERGED }
+ trait(:joined) { action Event::JOINED }
+ trait(:left) { action Event::LEFT }
+ trait(:destroyed) { action Event::DESTROYED }
+ trait(:expired) { action Event::EXPIRED }
+
factory :closed_issue_event do
action { Event::CLOSED }
target factory: :closed_issue
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 9e3f06c682c..2a2f3cca91c 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -6,12 +6,6 @@ FactoryGirl.define do
sequence(:position)
end
- factory :backlog_list, parent: :list do
- list_type :backlog
- label nil
- position nil
- end
-
factory :done_list, parent: :list do
list_type :done
label nil
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
new file mode 100644
index 00000000000..6d2e45f41ba
--- /dev/null
+++ b/spec/factories/pages_domains.rb
@@ -0,0 +1,153 @@
+FactoryGirl.define do
+ factory :pages_domain, class: 'PagesDomain' do
+ domain 'my.domain.com'
+
+ trait :with_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+YHi2yesCrOvVXt+lgPTd
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_key do
+ key '-----BEGIN PRIVATE KEY-----
+MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+nNp/xedE1YxutQ==
+-----END PRIVATE KEY-----'
+ end
+
+ trait :with_missing_chain do
+ # This certificate is signed with different key
+ # And misses the CA to build trust chain
+ certificate '-----BEGIN CERTIFICATE-----
+MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0
+IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS
+dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9
+ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ
+qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl
+Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A
+/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V
+Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud
+DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4
+QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI
+hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs
+Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h
+s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl
+WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF
+m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+
+VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_trusted_chain do
+ # This contains
+ # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA'
+ # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority'
+ certificate '-----BEGIN CERTIFICATE-----
+MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB
+hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV
+BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy
+MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT
+EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR
+Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh
+bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh
+bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0
+Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6
+ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51
+UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n
+c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY
+MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz
+30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV
+HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG
+BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv
+bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB
+AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E
+T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v
+ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p
+mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/
+e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps
+P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY
+dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc
+2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG
+V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4
+HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX
+j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII
+0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap
+lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf
++AZxAeKCINT+b72x
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv
+MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
+ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
+eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow
+gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
+BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD
+VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq
+hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw
+AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6
+2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr
+ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt
+4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq
+m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/
+vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT
+8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE
+IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO
+KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO
+GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/
+s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
+JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD
+AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9
+MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy
+bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6
+Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ
+zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj
+Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY
+Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5
+B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx
+PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR
+pu/xO28QOG8=
+-----END CERTIFICATE-----'
+ end
+
+ trait :with_expired_certificate do
+ certificate '-----BEGIN CERTIFICATE-----
+MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
+cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow
+HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF
+AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge
+NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS
+NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA
+ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X
+Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+
+Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9
+ZDXgrA==
+-----END CERTIFICATE-----'
+ end
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 992580a6b34..c80b09e9b9d 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -56,6 +56,25 @@ FactoryGirl.define do
end
end
+ trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED }
+ trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED }
+ trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE }
+ trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED }
+ trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED }
+ trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE }
+ trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED }
+ trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED }
+ trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE }
+ trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED }
+ trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED }
+ trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
+ trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
+ trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
+ trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
+ trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -106,6 +125,42 @@ FactoryGirl.define do
path { 'gitlabhq' }
test_repo
+
+ transient do
+ create_template nil
+ end
+
+ after :create do |project, evaluator|
+ TestEnv.copy_repo(project)
+
+ if evaluator.create_template
+ args = evaluator.create_template
+
+ project.add_user(args[:user], args[:access])
+
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/bug.md",
+ 'something valid',
+ message: 'test 3',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/template_test.md",
+ 'template_test',
+ message: 'test 1',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ args[:user],
+ ".gitlab/#{args[:path]}/feature_proposal.md",
+ 'feature_proposal',
+ message: 'test 2',
+ branch_name: 'master',
+ update: false)
+ end
+ end
end
factory :forked_project_with_submodules, parent: :empty_project do
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index 12fc4ec4486..6f1545418eb 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -4,6 +4,6 @@ FactoryGirl.define do
factory :timelog do
time_spent 3600
user
- association :trackable, factory: :issue
+ issue
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 91d6f39a5bf..b4e4cd97780 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -14,6 +14,10 @@ FactoryGirl.define do
action { Todo::MENTIONED }
end
+ trait :directly_addressed do
+ action { Todo::DIRECTLY_ADDRESSED }
+ end
+
trait :on_commit do
commit_id RepoHelpers.sample_commit.id
target_type "Commit"
@@ -24,6 +28,10 @@ FactoryGirl.define do
target factory: :merge_request
end
+ trait :marked do
+ action { Todo::MARKED }
+ end
+
trait :approval_required do
action { Todo::APPROVAL_REQUIRED }
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index c6f7869516e..1732b1a0081 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -14,6 +14,14 @@ FactoryGirl.define do
admin true
end
+ trait :blocked do
+ after(:build) { |user, _| user.block! }
+ end
+
+ trait :external do
+ external true
+ end
+
trait :two_factor do
two_factor_via_otp
end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index e177059d959..9d5ce876c29 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -9,8 +9,8 @@ describe 'Admin Builds' do
let(:pipeline) { create(:ci_pipeline) }
context 'All tab' do
- context 'when have builds' do
- it 'shows all builds' do
+ context 'when have jobs' do
+ it 'shows all jobs' do
create(:ci_build, pipeline: pipeline, status: :pending)
create(:ci_build, pipeline: pipeline, status: :running)
create(:ci_build, pipeline: pipeline, status: :success)
@@ -19,26 +19,26 @@ describe 'Admin Builds' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_selector('.row-content-block', text: 'All builds')
+ expect(page).to have_selector('.row-content-block', text: 'All jobs')
expect(page.all('.build-link').size).to eq(4)
expect(page).to have_link 'Cancel all'
end
end
- context 'when have no builds' do
+ context 'when have no jobs' do
it 'shows a message' do
visit admin_builds_path
expect(page).to have_selector('.nav-links li.active', text: 'All')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Pending tab' do
- context 'when have pending builds' do
- it 'shows pending builds' do
+ context 'when have pending jobs' do
+ it 'shows pending jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -55,22 +55,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds pending' do
+ context 'when have no jobs pending' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :pending)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Running tab' do
- context 'when have running builds' do
- it 'shows running builds' do
+ context 'when have running jobs' do
+ it 'shows running jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :running)
build2 = create(:ci_build, pipeline: pipeline, status: :success)
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
@@ -87,22 +87,22 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds running' do
+ context 'when have no jobs running' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :success)
visit admin_builds_path(scope: :running)
expect(page).to have_selector('.nav-links li.active', text: 'Running')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).not_to have_link 'Cancel all'
end
end
end
context 'Finished tab' do
- context 'when have finished builds' do
- it 'shows finished builds' do
+ context 'when have finished jobs' do
+ it 'shows finished jobs' do
build1 = create(:ci_build, pipeline: pipeline, status: :pending)
build2 = create(:ci_build, pipeline: pipeline, status: :running)
build3 = create(:ci_build, pipeline: pipeline, status: :success)
@@ -117,14 +117,14 @@ describe 'Admin Builds' do
end
end
- context 'when have no builds finished' do
+ context 'when have no jobs finished' do
it 'shows a message' do
create(:ci_build, pipeline: pipeline, status: :running)
visit admin_builds_path(scope: :finished)
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel all'
end
end
diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb
index eaa42aad0a7..6d6c9165c83 100644
--- a/spec/features/admin/admin_labels_spec.rb
+++ b/spec/features/admin/admin_labels_spec.rb
@@ -35,15 +35,16 @@ RSpec.describe 'admin issues labels' do
it 'deletes all labels', js: true do
page.within '.labels' do
page.all('.btn-remove').each do |remove|
- wait_for_ajax
remove.click
+ wait_for_ajax
end
end
- page.within '.manage-labels-list' do
- expect(page).not_to have_content('bug')
- expect(page).not_to have_content('feature_label')
- end
+ wait_for_ajax
+
+ expect(page).to have_content("There are no labels yet")
+ expect(page).not_to have_content('bug')
+ expect(page).not_to have_content('feature_label')
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index a586f8d3184..c0807b8c507 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -211,7 +211,7 @@ describe "Admin::Users", feature: true do
fill_in "user_email", with: "bigbang@mail.com"
fill_in "user_password", with: "AValidPassword1"
fill_in "user_password_confirmation", with: "AValidPassword1"
- check "user_admin"
+ choose "user_access_level_admin"
click_button "Save changes"
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
new file mode 100644
index 00000000000..2875fc1e533
--- /dev/null
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -0,0 +1,233 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:label) { create(:label, project: project) }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: label, position: 1) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue2) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ context 'modal interaction' do
+ it 'opens modal' do
+ click_button('Add issues')
+
+ expect(page).to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ find('.close').click
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+
+ it 'closes modal if cancel button clicked' do
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ click_button 'Cancel'
+ end
+
+ expect(page).not_to have_selector('.add-issues-modal')
+ end
+ end
+
+ context 'issues list' do
+ before do
+ click_button('Add issues')
+
+ wait_for_vue_resource
+ end
+
+ it 'loads issues' do
+ page.within('.add-issues-modal') do
+ page.within('.nav-links') do
+ expect(page).to have_content('2')
+ end
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows selected issues' do
+ page.within('.add-issues-modal') do
+ click_link 'Selected issues'
+
+ expect(page).not_to have_selector('.card')
+ end
+ end
+
+ context 'list dropdown' do
+ it 'resets after deleting list' do
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).to have_button(planning.title)
+
+ click_button 'Cancel'
+ end
+
+ first('.board-delete').click
+
+ click_button('Add issues')
+
+ wait_for_vue_resource
+
+ page.within('.add-issues-modal') do
+ expect(find('.add-issues-footer')).not_to have_button(planning.title)
+ expect(find('.add-issues-footer')).to have_button(label.title)
+ end
+ end
+ end
+
+ context 'search' do
+ it 'returns issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys(issue.title)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'returns no issues' do
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing search')
+
+ expect(page).not_to have_selector('.card')
+ expect(page).not_to have_content("You haven't added any issues to your project yet")
+ end
+ end
+ end
+
+ context 'selecing issues' do
+ it 'selects single issue' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ page.within('.nav-links') do
+ expect(page).to have_content('Selected issues 1')
+ end
+ end
+ end
+
+ it 'changes button text' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue')
+ end
+ end
+
+ it 'changes button text with plural' do
+ page.within('.add-issues-modal') do
+ all('.card').each do |el|
+ el.click
+ end
+
+ expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues')
+ end
+ end
+
+ it 'shows only selected issues on selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'selects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'deselects all issues' do
+ page.within('.add-issues-modal') do
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+
+ click_button 'Deselect all'
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+
+ it 'selects all that arent already selected' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ expect(page).to have_selector('.is-active', count: 1)
+
+ click_button 'Select all'
+
+ expect(page).to have_selector('.is-active', count: 2)
+ end
+ end
+
+ it 'unselects from selected tab' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_link 'Selected issues'
+
+ first('.card').click
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
+ end
+
+ context 'adding issues' do
+ it 'adds to board' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+
+ it 'adds to second list' do
+ page.within('.add-issues-modal') do
+ first('.card').click
+
+ click_button planning.title
+
+ click_link label.title
+
+ click_button 'Add 1 issue'
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page).to have_selector('.card')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index bfac5a1b8ab..7225f38b7e5 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
+ include DragTo
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
@@ -20,7 +21,7 @@ describe 'Issue Boards', feature: true, js: true do
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'shows blank state' do
@@ -31,18 +32,18 @@ describe 'Issue Boards', feature: true, js: true do
page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own")
end
- expect(page).to have_selector('.board', count: 2)
+ expect(page).to have_selector('.board', count: 1)
end
it 'creates default lists' do
- lists = ['Backlog', 'To Do', 'Doing', 'Done']
+ lists = ['To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
@@ -64,42 +65,41 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
- let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
- let!(:issue1) { create(:issue, project: project, assignee: user) }
- let!(:issue2) { create(:issue, project: project, author: user2) }
- let!(:issue3) { create(:issue, project: project) }
- let!(:issue4) { create(:issue, project: project) }
+ let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
+ let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
+ let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
expect(find('.board:nth-child(1)')).to have_selector('.card')
expect(find('.board:nth-child(2)')).to have_selector('.card')
expect(find('.board:nth-child(3)')).to have_selector('.card')
- expect(find('.board:nth-child(4)')).to have_selector('.card')
end
it 'shows lists' do
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 3)
end
it 'shows description tooltip on list title' do
- page.within('.board:nth-child(2)') do
+ page.within('.board:nth-child(1)') do
expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
end
end
it 'shows issues in lists' do
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
end
it 'shows confidential issues with icon' do
@@ -108,19 +108,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'search backlog list' do
- page.within('#js-boards-search') do
- find('.form-control').set(issue1.title)
- end
-
- wait_for_vue_resource
-
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
- end
-
it 'search done list' do
page.within('#js-boards-search') do
find('.form-control').set(issue8.title)
@@ -130,8 +117,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
end
it 'search list' do
@@ -141,157 +127,135 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
end
it 'allows user to delete board' do
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'removes checkmark in new list dropdown after deleting' do
click_button 'Add list'
wait_for_ajax
- page.within(find('.board:nth-child(2)')) do
+ page.within(find('.board:nth-child(1)')) do
find('.board-delete').click
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
- expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ expect(page).to have_selector('.board', count: 2)
end
it 'infinite scrolls list' do
50.times do
- create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
end
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
page.within(find('.board', match: :first)) do
- expect(page.find('.board-header')).to have_content('56')
+ expect(page.find('.board-header')).to have_content('58')
expect(page).to have_selector('.card', count: 20)
- expect(page).to have_content('Showing 20 of 56 issues')
+ expect(page).to have_content('Showing 20 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
expect(page).to have_selector('.card', count: 40)
- expect(page).to have_content('Showing 40 of 56 issues')
+ expect(page).to have_content('Showing 40 of 58 issues')
evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
wait_for_vue_resource
- expect(page).to have_selector('.card', count: 56)
+ expect(page).to have_selector('.card', count: 58)
expect(page).to have_content('Showing all issues')
end
end
- context 'backlog' do
- it 'shows issues in backlog with no labels' do
- wait_for_board_cards(1, 6)
- end
-
- it 'moves issue from backlog into list' do
- drag_to(list_to_index: 1)
-
- wait_for_vue_resource
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 3)
- end
- end
-
context 'done' do
it 'shows list of done issues' do
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(3, 1)
wait_for_ajax
end
it 'moves issue to done' do
- drag_to(list_from_index: 0, list_to_index: 3)
+ drag(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 5)
+ wait_for_board_cards(1, 7)
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 2)
expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
- expect(find('.board:nth-child(4)')).to have_content(issue9.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
it 'removes all of the same issue to done' do
- drag_to(list_from_index: 1, list_to_index: 3)
+ drag(list_from_index: 0, list_to_index: 2)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 2)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 2)
- expect(find('.board:nth-child(2)')).not_to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).to have_content(issue6.title)
- expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).not_to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
end
context 'lists' do
it 'changes position of list' do
- drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+ drag(list_from_index: 1, list_to_index: 0, selector: '.board-header')
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 2)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 2)
+ wait_for_board_cards(2, 8)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(2)')).to have_content(development.title)
- expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(development.title)
+ expect(find('.board:nth-child(1)')).to have_content(planning.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+ drag(list_from_index: 0, from_index: 1, list_to_index: 1)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 1)
- wait_for_board_cards(3, 3)
- wait_for_board_cards(4, 1)
+ wait_for_board_cards(1, 7)
+ wait_for_board_cards(2, 2)
+ wait_for_board_cards(3, 1)
- expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title)
end
it 'issue moves between lists' do
- drag_to(list_from_index: 2, list_to_index: 1)
+ drag(list_from_index: 1, list_to_index: 0)
- wait_for_board_cards(1, 6)
- wait_for_board_cards(2, 3)
+ wait_for_board_cards(1, 9)
+ wait_for_board_cards(2, 1)
wait_for_board_cards(3, 1)
- wait_for_board_cards(4, 1)
- expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ expect(find('.board:nth-child(1)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)
end
it 'issue moves from done' do
- drag_to(list_from_index: 3, list_to_index: 1)
+ drag(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
- wait_for_board_cards(1, 6)
+ wait_for_board_cards(1, 8)
wait_for_board_cards(2, 3)
- wait_for_board_cards(3, 2)
- wait_for_board_cards(4, 0)
+ wait_for_board_cards(3, 0)
end
context 'issue card' do
@@ -324,7 +288,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Backlog label' do
@@ -337,7 +301,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'creates new list for Done label' do
@@ -350,7 +314,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
it 'keeps dropdown open after adding new list' do
@@ -366,21 +330,6 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.issue-boards-search')).to have_selector('.open')
end
- it 'moves issues from backlog into new list' do
- wait_for_board_cards(1, 6)
-
- click_button 'Add list'
- wait_for_ajax
-
- page.within('.dropdown-menu-issues-board-new') do
- click_link testing.title
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- end
-
it 'creates new list from a new label' do
click_button 'Add list'
@@ -397,7 +346,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 4)
end
end
end
@@ -418,7 +367,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by assignee' do
@@ -437,7 +386,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by milestone' do
@@ -454,10 +403,9 @@ describe 'Issue Boards', feature: true, js: true do
end
wait_for_vue_resource
- wait_for_board_cards(1, 0)
- wait_for_board_cards(2, 1)
+ wait_for_board_cards(1, 1)
+ wait_for_board_cards(2, 0)
wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 0)
end
it 'filters by label' do
@@ -474,7 +422,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
end
it 'filters by label with space after reload' do
@@ -530,7 +478,7 @@ describe 'Issue Boards', feature: true, js: true do
it 'infinite scrolls list with label filter' do
50.times do
- create(:labeled_issue, project: project, labels: [testing])
+ create(:labeled_issue, project: project, labels: [planning, testing])
end
page.within '.issues-filters' do
@@ -580,32 +528,12 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
- end
-
- it 'filters by no label' do
- page.within '.issues-filters' do
- click_button('Label')
- wait_for_ajax
-
- page.within '.dropdown-menu-labels' do
- click_link("No Label")
- wait_for_vue_resource
- find('.dropdown-menu-close').click
- end
- end
-
- wait_for_vue_resource
-
- wait_for_board_cards(1, 5)
- wait_for_board_cards(2, 0)
- wait_for_board_cards(3, 0)
- wait_for_board_cards(4, 1)
+ wait_for_empty_boards((2..3))
end
it 'filters by clicking label button on issue' do
page.within(find('.board', match: :first)) do
- expect(page).to have_selector('.card', count: 6)
+ expect(page).to have_selector('.card', count: 8)
expect(find('.card', match: :first)).to have_content(bug.title)
click_button(bug.title)
wait_for_vue_resource
@@ -614,7 +542,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_vue_resource
wait_for_board_cards(1, 1)
- wait_for_empty_boards((2..4))
+ wait_for_empty_boards((2..3))
page.within('.labels-filter') do
expect(find('.dropdown-toggle-text')).to have_content(bug.title)
@@ -688,14 +616,13 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
- evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
-
- Timeout.timeout(Capybara.default_max_wait_time) do
- loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
- end
-
- wait_for_vue_resource
+ def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index)
end
def wait_for_board_cards(board_number, expected_cards)
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
new file mode 100644
index 00000000000..1cf0d11d448
--- /dev/null
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -0,0 +1,259 @@
+require 'rails_helper'
+
+describe 'Issue Boards add issue modal filtering', :feature, :js do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:issue1) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
+ it 'shows empty state when no results found' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ find('.form-control').native.send_keys('testing empty state')
+
+ wait_for_vue_resource
+
+ expect(page).to have_content('There are no issues to show.')
+ end
+ end
+
+ it 'restores filters when closing' do
+ visit_board
+
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Upcoming'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 0)
+
+ click_button 'Cancel'
+ end
+
+ click_button('Add issues')
+
+ page.within('.add-issues-modal') do
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ context 'author' do
+ let!(:issue) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ visit_board
+ end
+
+ it 'filters by any author' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link 'Any Author'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by selected user' do
+ page.within('.add-issues-modal') do
+ click_button 'Author'
+
+ wait_for_ajax
+
+ click_link user2.name
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'assignee' do
+ let!(:issue) { create(:issue, project: project, assignee: user2) }
+
+ before do
+ project.team << [user2, :developer]
+
+ visit_board
+ end
+
+ it 'filters by any assignee' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ click_link 'Any Assignee'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by unassigned' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ click_link 'Unassigned'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by selected user' do
+ page.within('.add-issues-modal') do
+ click_button 'Assignee'
+
+ wait_for_ajax
+
+ page.within '.dropdown-menu-user' do
+ click_link user2.name
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+
+ before do
+ visit_board
+ end
+
+ it 'filters by any milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Any Milestone'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by upcoming milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link 'Upcoming'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by selected milestone' do
+ page.within('.add-issues-modal') do
+ click_button 'Milestone'
+
+ wait_for_ajax
+
+ click_link milestone.name
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ context 'label' do
+ let(:label) { create(:label, project: project) }
+ let!(:issue) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before do
+ visit_board
+ end
+
+ it 'filters by any label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link 'Any Label'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'filters by no label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link 'No Label'
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by label' do
+ page.within('.add-issues-modal') do
+ click_button 'Label'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+ end
+
+ def visit_board
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ click_button('Add issues')
+ end
+end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index a03cd6fbf2d..6d14a8cf483 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
let(:project) { create(:empty_project, :public) }
let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) }
context 'authorized user' do
@@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
visit namespace_project_board_path(project.namespace, project, board)
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 2)
end
it 'displays new issue button' do
@@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
end
it 'does not display new issue button in done list' do
- page.within('.board:nth-child(3)') do
+ page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 188d33e8ef4..7651364703e 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:empty_project, :public) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let!(:label) { create(:label, project: project) }
- let!(:label2) { create(:label, project: project) }
- let!(:milestone) { create(:milestone, project: project) }
- let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
- let!(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:development) { create(:label, project: project, name: 'Development') }
+ let!(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:regression) { create(:label, project: project, name: 'Regression') }
+ let!(:stretch) { create(:label, project: project, name: 'Stretch') }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
+ let(:board) { create(:board, project: project) }
+ let!(:list) { create(:list, board: board, label: development, position: 0) }
before do
project.team << [user, :master]
@@ -51,7 +54,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.issue-boards-sidebar')
- find('.gutter-toggle').click
+ find('.gutter-toggle').trigger('click')
expect(page).not_to have_selector('.issue-boards-sidebar')
end
@@ -62,8 +65,22 @@ describe 'Issue Boards', feature: true, js: true do
end
page.within('.issue-boards-sidebar') do
- expect(page).to have_content(issue.title)
- expect(page).to have_content(issue.to_reference)
+ expect(page).to have_content(issue2.title)
+ expect(page).to have_content(issue2.to_reference)
+ end
+ end
+
+ it 'removes card from board when clicking remove button' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.issue-boards-sidebar') do
+ click_button 'Remove from board'
+ end
+
+ page.within(first('.board')) do
+ expect(page).to have_selector('.card', count: 1)
end
end
@@ -141,6 +158,36 @@ describe 'Issue Boards', feature: true, js: true do
end
end
end
+
+ it 'resets assignee dropdown' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link user.name
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ expect(page).not_to have_selector('.is-active')
+ end
+ end
end
context 'milestone' do
@@ -194,7 +241,7 @@ describe 'Issue Boards', feature: true, js: true do
page.within('.due_date') do
click_link 'Edit'
- click_link Date.today.day
+ click_button Date.today.day
wait_for_vue_resource
@@ -214,22 +261,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
+ click_link bug.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
+ expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_content(bug.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 1)
- expect(page).to have_content(label.title)
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(bug.title)
end
end
end
@@ -244,32 +291,32 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
- click_link label2.title
+ click_link bug.title
+ click_link regression.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
+ expect(page).to have_selector('.label', count: 4)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
end
end
page.within(first('.board')) do
page.within(first('.card')) do
- expect(page).to have_selector('.label', count: 2)
- expect(page).to have_content(label.title)
- expect(page).to have_content(label2.title)
+ expect(page).to have_selector('.label', count: 3)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
end
end
end
it 'removes a label' do
page.within(first('.board')) do
- find('.card:nth-child(2)').click
+ first('.card').click
end
page.within('.labels') do
@@ -277,22 +324,22 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
- click_link label.title
+ click_link stretch.title
wait_for_vue_resource
find('.dropdown-menu-close-icon').click
page.within('.value') do
- expect(page).to have_selector('.label', count: 0)
- expect(page).not_to have_content(label.title)
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).not_to have_content(stretch.title)
end
end
page.within(first('.board')) do
- page.within(find('.card:nth-child(2)')) do
- expect(page).not_to have_selector('.label', count: 1)
- expect(page).not_to have_content(label.title)
+ page.within(first('.card')) do
+ expect(page).not_to have_selector('.label')
+ expect(page).not_to have_content(stretch.title)
end
end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index f3a5b565122..fec86128d03 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -251,6 +251,8 @@ describe 'Copy as GFM', feature: true, js: true do
'SanitizationFilter',
<<-GFM.strip_heredoc
+ <a name="named-anchor"></a>
+
<sub>sub</sub>
<dl>
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
new file mode 100644
index 00000000000..d9be4e5dbdd
--- /dev/null
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard shortcuts', feature: true, js: true do
+ before do
+ login_as :user
+ visit dashboard_projects_path
+ end
+
+ scenario 'Navigate to tabs' do
+ find('body').native.send_key('g')
+ find('body').native.send_key('p')
+
+ ensure_active_main_tab('Projects')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('i')
+
+ ensure_active_main_tab('Issues')
+
+ find('body').native.send_key('g')
+ find('body').native.send_key('m')
+
+ ensure_active_main_tab('Merge Requests')
+ end
+
+ def ensure_active_main_tab(content)
+ expect(find('.nav-sidebar li.active')).to have_content(content)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index 56f6cd2e095..c203e1f20c1 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -19,6 +19,10 @@ feature 'Environment', :feature do
visit_environment(environment)
end
+ scenario 'shows environment name' do
+ expect(page).to have_content(environment.name)
+ end
+
context 'without deployments' do
scenario 'does show no deployments' do
expect(page).to have_content('You don\'t have any deployments right now.')
@@ -60,10 +64,6 @@ feature 'Environment', :feature do
expect(page).to have_link('Re-deploy')
end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
- end
-
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -101,6 +101,22 @@ feature 'Environment', :feature do
scenario 'it shows the terminal button' do
expect(page).to have_terminal_button
end
+
+ context 'web terminal', :js do
+ before do
+ # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
+ allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ visit terminal_namespace_project_environment_path(project.namespace, project, environment)
+ end
+
+ it 'displays a web terminal' do
+ expect(page).to have_selector('#terminal')
+ end
+
+ it 'displays a link to the environment external url' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
context 'for developer' do
@@ -112,28 +128,44 @@ feature 'Environment', :feature do
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 'when environment is available' do
+ 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_link('Stop')
+ end
- scenario 'does allow to stop environment' do
- click_link('Stop')
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
- 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
+ end
+ end
- scenario 'does not show stop button' do
- expect(page).not_to have_link('Stop')
+ context 'without stop action' do
+ scenario 'does allow to stop environment' do
+ click_link('Stop')
end
end
end
+
+ context 'when environment is stopped' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not show stop button' do
+ expect(page).not_to have_link('Stop')
+ end
+ end
end
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 72b984cfab8..78be7d36f47 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
end
+
+ context 'for available environment' do
+ given(:environment) { create(:environment, project: project, state: :available) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
+
+ context 'for stopped environment' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ scenario 'does not shows stop button' do
+ expect(page).not_to have_selector('.stop-env-link')
+ end
+ end
end
context 'with deployments' do
@@ -194,7 +210,7 @@ feature 'Environments page', :feature, :js do
end
scenario 'does create a new pipeline' do
- expect(page).to have_content('Production')
+ expect(page).to have_content('production')
end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8b3e2fa93a2..8c64b050e19 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -72,8 +72,8 @@ feature 'Expand and collapse diffs', js: true, feature: true do
it 'collapses large diffs for renamed files by default' do
expect(large_diff_renamed).not_to have_selector('.code')
expect(large_diff_renamed).to have_selector('.nothing-here-block')
- expect(large_diff_renamed).to have_selector('.file-title .deletion')
- expect(large_diff_renamed).to have_selector('.file-title .addition')
+ expect(large_diff_renamed).to have_selector('.js-file-title .deletion')
+ expect(large_diff_renamed).to have_selector('.js-file-title .addition')
end
it 'shows non-renderable diffs as such immediately, regardless of their size' do
@@ -115,9 +115,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a large diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `large_diff.md` title
- all('.file-title')[1].click
+ all('.diff-toggle-caret')[1].click
wait_for_ajax
end
@@ -159,9 +159,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding the diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `large_diff.md` title
- all('.file-title')[1].click
+ all('.diff-toggle-caret')[1].click
wait_for_ajax
end
@@ -181,9 +181,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'collapsing an expanded diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'hides the diff content' do
@@ -194,9 +194,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 're-expanding the same diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'shows the diff content' do
@@ -290,9 +290,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'collapsing an expanded diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'hides the diff content' do
@@ -303,9 +303,9 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 're-expanding the same diff' do
before do
# Wait for diffs
- find('.file-title', match: :first)
+ find('.js-file-title', match: :first)
# Click `small_diff.md` title
- all('.file-title')[3].click
+ all('.diff-toggle-caret')[3].click
end
it 'shows the diff content' do
diff --git a/spec/features/groups/members/list_spec.rb b/spec/features/groups/members/list_spec.rb
new file mode 100644
index 00000000000..109de39b2dd
--- /dev/null
+++ b/spec/features/groups/members/list_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Groups members list', feature: true do
+ let(:user1) { create(:user, name: 'John Doe') }
+ let(:user2) { create(:user, name: 'Mary Jane') }
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ background do
+ login_as(user1)
+ end
+
+ scenario 'show members from current group and parent' do
+ group.add_developer(user1)
+ nested_group.add_developer(user2)
+
+ visit group_group_members_path(nested_group)
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row.text).to include(user2.name)
+ end
+
+ scenario 'show user once if member of both current group and parent' do
+ group.add_developer(user1)
+ nested_group.add_developer(user1)
+
+ visit group_group_members_path(nested_group)
+
+ expect(first_row.text).to include(user1.name)
+ expect(second_row).to be_blank
+ end
+
+ def first_row
+ page.all('ul.content-list > li')[0]
+ end
+
+ def second_row
+ page.all('ul.content-list > li')[1]
+ end
+end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 78a11ffee99..b55078c3bf6 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
- let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) }
+ let(:project_archived) { create(:project, :archived, :merge_requests_enabled, group: group) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index a515c92db37..37b7c20239f 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -45,6 +45,23 @@ feature 'Group', feature: true do
end
end
+ describe 'create a nested group' do
+ let(:group) { create(:group, path: 'foo') }
+
+ before do
+ visit subgroups_group_path(group)
+ click_link 'New Subgroup'
+ end
+
+ it 'creates a nested group' do
+ fill_in 'Group path', with: 'bar'
+ click_button 'Create group'
+
+ expect(current_path).to eq(group_path('foo/bar'))
+ expect(page).to have_content("Group 'bar' was successfully created.")
+ end
+ end
+
describe 'group edit' do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
@@ -117,7 +134,7 @@ feature 'Group', feature: true do
visit path
click_link 'Subgroups'
- expect(page).to have_content(nested_group.full_name)
+ expect(page).to have_content(nested_group.name)
end
end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
new file mode 100644
index 00000000000..e31bc40adc3
--- /dev/null
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+describe 'issuable list', feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ issuable_types = [:issue, :merge_request]
+
+ before do
+ project.add_user(user, :developer)
+ login_as(user)
+ issuable_types.each { |type| create_issuables(type) }
+ end
+
+ issuable_types.each do |issuable_type|
+ it "avoids N+1 database queries for #{issuable_type.to_s.humanize.pluralize}" do
+ control_count = ActiveRecord::QueryRecorder.new { visit_issuable_list(issuable_type) }.count
+
+ create_issuables(issuable_type)
+
+ expect { visit_issuable_list(issuable_type) }.not_to exceed_query_limit(control_count)
+ end
+
+ it "counts upvotes, downvotes and notes count for each #{issuable_type.to_s.humanize}" do
+ visit_issuable_list(issuable_type)
+
+ expect(first('.fa-thumbs-up').find(:xpath, '..')).to have_content(1)
+ expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1)
+ expect(first('.fa-comments').find(:xpath, '..')).to have_content(2)
+ end
+ end
+
+ def visit_issuable_list(issuable_type)
+ if issuable_type == :issue
+ visit namespace_project_issues_path(project.namespace, project)
+ else
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+ end
+
+ def create_issuables(issuable_type)
+ 3.times do
+ if issuable_type == :issue
+ issuable = create(:issue, project: project, author: user)
+ else
+ issuable = create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
+
+ 2.times do
+ create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
+ end
+
+ create(:award_emoji, :downvote, awardable: issuable)
+ create(:award_emoji, :upvote, awardable: issuable)
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 5079eb8dd00..c6a88e1b7b0 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -1,39 +1,47 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Dropdown label', js: true, feature: true do
- include WaitForAjax
-
- let!(:project) { create(:empty_project) }
- let!(:user) { create(:user) }
- let!(:bug_label) { create(:label, project: project, title: 'bug') }
- let!(:uppercase_label) { create(:label, project: project, title: 'BUG') }
- let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
- let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
- let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
- let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')}
- let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')}
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_label) { '#js-dropdown-label' }
+ let(:filter_dropdown) { find("#{js_dropdown_label} .filter-dropdown") }
+
+ shared_context 'with labels' do
+ let!(:bug_label) { create(:label, project: project, title: 'bug-label') }
+ let!(:uppercase_label) { create(:label, project: project, title: 'BUG-LABEL') }
+ let!(:two_words_label) { create(:label, project: project, title: 'High Priority') }
+ let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') }
+ let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') }
+ let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()') }
+ let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title') }
+ end
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 3
- wait_for_ajax
- sleep 3
- end
+ def init_label_search
+ filtered_search.set('label:')
+ # This ensures the dropdown is shown
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
- def dropdown_label_size
- page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size
+ def search_for_label(label)
+ init_label_search
+ filtered_search.send_keys(label)
end
def click_label(text)
- find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click
+ filter_dropdown.find('.filter-dropdown-item', text: text).click
+ end
+
+ def dropdown_label_size
+ filter_dropdown.all('.filter-dropdown-item').size
+ end
+
+ def clear_search_field
+ find('.filtered-search-input-container .clear-search').click
end
before do
- project.team << [user, :master]
+ project.add_master(user)
login_as(user)
create(:issue, project: project)
@@ -42,11 +50,12 @@ describe 'Dropdown label', js: true, feature: true do
describe 'keyboard navigation' do
it 'selects label' do
- send_keys_to_filtered_search('label:')
+ bug_label = create(:label, project: project, title: 'bug-label')
+ init_label_search
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq("label:~#{special_label.name} ")
+ expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
end
@@ -54,171 +63,177 @@ describe 'Dropdown label', js: true, feature: true do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+ expect(page).to have_css(js_dropdown_label)
end
it 'closes when the search bar is unfocused' do
- find('body').click()
+ find('body').click
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened and hides it when loaded' do
filtered_search.set('label:')
- expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true)
- end
-
- it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('label:')
-
- expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading')
+ expect(find(js_dropdown_label)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_label)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the labels when opened' do
- send_keys_to_filtered_search('label:')
+ it 'loads all the labels when opened' do
+ bug_label = create(:label, project: project, title: 'bug-label')
+ filtered_search.set('label:')
- expect(dropdown_label_size).to be > 0
+ expect(filter_dropdown).to have_content(bug_label.title)
+ expect(dropdown_label_size).to eq(1)
end
end
describe 'filtering' do
- before do
- filtered_search.set('label')
- end
-
- it 'filters by name' do
- send_keys_to_filtered_search(':b')
+ include_context 'with labels'
- expect(dropdown_label_size).to eq(2)
+ before do
+ init_label_search
end
- it 'filters by case insensitive name' do
- send_keys_to_filtered_search(':B')
+ it 'filters by case-insensitive name with or without symbol' do
+ search_for_label('b')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
expect(dropdown_label_size).to eq(2)
- end
- it 'filters by name with symbol' do
- send_keys_to_filtered_search(':~bu')
+ clear_search_field
+ init_label_search
- expect(dropdown_label_size).to eq(2)
- end
-
- it 'filters by case insensitive name with symbol' do
- send_keys_to_filtered_search(':~BU')
+ search_for_label('~bu')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
expect(dropdown_label_size).to eq(2)
end
- it 'filters by multiple words' do
- send_keys_to_filtered_search(':Hig')
+ it 'filters by multiple words with or without symbol' do
+ filtered_search.send_keys('Hig')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words with symbol' do
- send_keys_to_filtered_search(':~Hig')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~Hig')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: two_words_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing single quotes' do
- send_keys_to_filtered_search(':won\'t')
+ it 'filters by multiple words containing single quotes with or without symbol' do
+ filtered_search.send_keys('won\'t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing single quotes with symbol' do
- send_keys_to_filtered_search(':~won\'t')
+ clear_search_field
+ init_label_search
+
+ filtered_search.send_keys('~won\'t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_single_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by multiple words containing double quotes' do
- send_keys_to_filtered_search(':won"t')
+ it 'filters by multiple words containing double quotes with or without symbol' do
+ filtered_search.send_keys('won"t')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by multiple words containing double quotes with symbol' do
- send_keys_to_filtered_search(':~won"t')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~won"t')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: wont_fix_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
- it 'filters by special characters' do
- send_keys_to_filtered_search(':^+')
+ it 'filters by special characters with or without symbol' do
+ filtered_search.send_keys('^+')
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
- end
- it 'filters by special characters with symbol' do
- send_keys_to_filtered_search(':~^+')
+ clear_search_field
+ init_label_search
+ filtered_search.send_keys('~^+')
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: special_label.title)).to be_visible
expect(dropdown_label_size).to eq(1)
end
end
describe 'selecting from dropdown' do
+ include_context 'with labels'
+
before do
- filtered_search.set('label:')
+ init_label_search
end
it 'fills in the label name when the label has not been filled' do
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name when the label is partially filled' do
- send_keys_to_filtered_search('bu')
+ filtered_search.send_keys('bu')
click_label(bug_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:~#{special_label.title} ")
end
it 'selects `no label`' do
- find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
- expect(page).to have_css(js_dropdown_label, visible: false)
+ expect(page).not_to have_css(js_dropdown_label)
expect(filtered_search.value).to eq("label:none ")
end
end
@@ -226,44 +241,47 @@ describe 'Dropdown label', js: true, feature: true do
describe 'input has existing content' do
it 'opens label dropdown with existing search term' do
filtered_search.set('searchTerm label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing author' do
filtered_search.set('author:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing assignee' do
filtered_search.set('assignee:@person label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing label' do
filtered_search.set('label:~urgent label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
it 'opens label dropdown with existing milestone' do
filtered_search.set('milestone:%v2.0 label:')
- expect(page).to have_css(js_dropdown_label, visible: true)
+
+ expect(page).to have_css(js_dropdown_label)
end
end
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
- initial_size = dropdown_label_size
+ create(:label, project: project, title: 'bug-label')
+ init_label_search
- expect(initial_size).to be > 0
+ expect(dropdown_label_size).to eq(1)
create(:label, project: project)
- find('.filtered-search-input-container .clear-search').click
- filtered_search.set('label')
- send_keys_to_filtered_search(':')
+ clear_search_field
+ init_label_search
- expect(dropdown_label_size).to eq(initial_size)
+ expect(dropdown_label_size).to eq(1)
end
end
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 3f70a6aa75f..6f7046c8461 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -801,4 +801,26 @@ describe 'Filter issues', js: true, feature: true do
expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
end
end
+
+ context 'URL has a trailing slash' do
+ before do
+ visit "#{namespace_project_issues_path(project.namespace, project)}/"
+ end
+
+ it 'milestone dropdown loads milestones' do
+ input_filtered_search("milestone:", submit: false)
+
+ within('#js-dropdown-milestone') do
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 2)
+ end
+ end
+
+ it 'label dropdown load labels' do
+ input_filtered_search("label:", submit: false)
+
+ within('#js-dropdown-label') do
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
+ end
+ end
+ end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 31156fcf994..93139dc9e94 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
feature 'GFM autocomplete', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user, username: 'someone.special') }
+ let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
let(:issue) { create(:issue, project: project) }
@@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(find('#at-view-64')).to have_selector('.cur:first-of-type')
end
+ it 'includes items for assignee dropdowns with non-ASCII characters in name' do
+ page.within '.timeline-content-form' do
+ find('#note_note').native.send_keys('')
+ find('#note_note').native.send_keys("@#{user.name[0...8]}")
+ end
+
+ expect(page).to have_selector('.atwho-container')
+
+ wait_for_ajax
+
+ expect(find('#at-view-64')).to have_content(user.name)
+ end
+
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys('')
diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb
new file mode 100644
index 00000000000..fc8515cfe9b
--- /dev/null
+++ b/spec/features/issues/group_label_sidebar_spec.rb
@@ -0,0 +1,21 @@
+require 'rails_helper'
+
+describe 'Group label on issue', :feature do
+ it 'renders link to the project issues page' do
+ group = create(:group)
+ project = create(:empty_project, :public, namespace: group)
+ feature = create(:group_label, group: group, title: 'feature')
+ issue = create(:labeled_issue, project: project, labels: [feature])
+ label_link = namespace_project_issues_path(
+ project.namespace,
+ project,
+ label_name: [feature.name]
+ )
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ link = find('.issuable-show-labels a')
+
+ expect(link[:href]).to eq(label_link)
+ end
+end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index a4d3053d10c..c0ab42c6822 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Start new branch from an issue', feature: true do
+feature 'Start new branch from an issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
@@ -11,7 +11,7 @@ feature 'Start new branch from an issue', feature: true do
login_as(user)
end
- it 'shows the new branch button', js: true do
+ it 'shows the new branch button' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).to have_css('#new-branch .available')
@@ -34,16 +34,26 @@ feature 'Start new branch from an issue', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it "hides the new branch button", js: true do
+ it "hides the new branch button" 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
end
+
+ context 'when issue is confidential' do
+ it 'hides the new branch button' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).not_to have_css('#new-branch')
+ end
+ end
end
- context "for visiters" do
- it 'shows no buttons', js: true do
+ context 'for visitors' do
+ it 'shows no buttons' do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
new file mode 100644
index 00000000000..4bc9b49f889
--- /dev/null
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -0,0 +1,66 @@
+require 'rails_helper'
+
+describe 'New issue', feature: true do
+ include StubENV
+
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user)}
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+
+ current_application_settings.update!(
+ akismet_enabled: true,
+ akismet_api_key: 'testkey',
+ recaptcha_enabled: true,
+ recaptcha_site_key: 'test site key',
+ recaptcha_private_key: 'test private key'
+ )
+
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue after solving reCaptcha' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
+ # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('issue title')
+ expect(page).to have_css('.recaptcha')
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+
+ context 'when not identified as a spam' do
+ before do
+ WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
+
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'creates an issue' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 394eb31aff8..755162a1eb5 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -78,8 +78,8 @@ describe 'Issues', feature: true do
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
- page.within '.ui-datepicker' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -110,8 +110,8 @@ describe 'Issues', feature: true do
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
- page.within '.ui-datepicker' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -624,8 +624,8 @@ describe 'Issues', feature: true do
page.within '.due_date' do
click_link 'Edit'
- page.within '.ui-datepicker-calendar' do
- click_link date.day
+ page.within '.pika-single' do
+ click_button date.day
end
wait_for_ajax
@@ -635,11 +635,13 @@ describe 'Issues', feature: true do
end
it 'removes due date from issue' do
+ date = Date.today.at_beginning_of_month + 2.days
+
page.within '.due_date' do
click_link 'Edit'
- page.within '.ui-datepicker-calendar' do
- first('.ui-state-default').click
+ page.within '.pika-single' do
+ click_button date.day
end
wait_for_ajax
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 76bcfbe523a..ae609160e18 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -25,6 +25,27 @@ feature 'Login', feature: true do
expect(current_path).to eq root_path
end
+
+ it 'does not show flash messages when login page' do
+ visit root_path
+ expect(page).not_to have_content('You need to sign in or sign up before continuing.')
+ end
+ end
+
+ describe 'with a blocked account' do
+ it 'prevents the user from logging in' do
+ user = create(:user, :blocked)
+
+ login_with(user)
+
+ expect(page).to have_content('Your account has been blocked.')
+ end
+
+ it 'does not update Devise trackable attributes' do
+ user = create(:user, :blocked)
+
+ expect { login_with(user) }.not_to change { user.reload.sign_in_count }
+ end
end
describe 'with two-factor authentication' do
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 5bc4ab2dfe5..d710a780111 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -141,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
click_on 'Changes'
wait_for_ajax
- find('.click-to-expand').click
+ click_link 'Expand all'
wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index f1b68a39343..e853fb7e016 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -84,4 +84,24 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
+
+ context 'when a new merge request has a pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, sha: project.commit('fix').id,
+ ref: 'fix',
+ project: project)
+ end
+
+ it 'shows pipelines for a new merge request' do
+ visit new_namespace_project_merge_request_path(
+ project.namespace, project,
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ page.within('.merge-request') do
+ click_link 'Pipelines'
+
+ expect(page).to have_content "##{pipeline.id}"
+ end
+ end
+ end
end
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..84ad8765d8f
--- /dev/null
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -0,0 +1,100 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
+ let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
+
+ before do
+ build.run
+
+ login_as(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ describe 'build list toggle' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ it 'should expand when hovered' do
+ before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ toggle.hover
+
+ after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
+
+ expect(before_width).to be < after_width
+ end
+
+ it 'should show dropdown caret when hovered' do
+ toggle.hover
+
+ expect(toggle).to have_selector('.fa-caret-down')
+ end
+
+ it 'should show tooltip when hovered' do
+ toggle.hover
+
+ expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+
+ describe 'builds list menu' do
+ let(:toggle) do
+ find('.mini-pipeline-graph-dropdown-toggle')
+ first('.mini-pipeline-graph-dropdown-toggle')
+ end
+
+ before do
+ toggle.click
+ wait_for_ajax
+ end
+
+ it 'should open when toggle is clicked' do
+ expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when toggle is clicked again' do
+ toggle.trigger('click')
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ it 'should close when clicking somewhere else' do
+ find('body').click
+
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
+
+ describe 'build list build item' do
+ let(:build_item) do
+ find('.mini-pipeline-graph-dropdown-item')
+ first('.mini-pipeline-graph-dropdown-item')
+ end
+
+ it 'should visit the build page when clicked' do
+ build_item.click
+ find('.build-page')
+
+ expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
+ end
+
+ it 'should show tooltip when hovered' do
+ build_item.hover
+
+ expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 7e2907cd26f..d2f5c4afc93 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
@@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature:
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept Merge Request'
- expect(page).to have_content('Please retry the build or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
new file mode 100644
index 00000000000..a2cf9b18bf2
--- /dev/null
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'toggler_behavior', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: user) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:fragment_id) { "#note_#{note.id}" }
+
+ before do
+ login_as :admin
+ project = merge_request.source_project
+ page.current_window.resize_to(1000, 300)
+ visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}"
+ end
+
+ describe 'scroll position' do
+ it 'should be scrolled down to fragment' do
+ page_height = page.current_window.size[1]
+ page_scroll_y = page.evaluate_script("window.scrollY")
+ fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top")
+ expect(find('.js-toggle-content').visible?).to eq true
+ expect(find(fragment_id).visible?).to eq true
+ expect(fragment_position_top).to be >= page_scroll_y
+ expect(fragment_position_top).to be < (page_scroll_y + page_height)
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index b13674b4db9..2f3c3e45ae6 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
end
describe 'merge-request-only commands' do
@@ -120,5 +120,81 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
expect(page).not_to have_content '/due 2016-08-28'
end
end
+
+ describe '/target_branch command in merge request' do
+ let(:another_project) { create(:project, :public) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+
+ before do
+ logout
+ another_project.team << [user, :master]
+ login_with(user)
+ end
+
+ it 'changes target_branch in new merge_request' do
+ visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
+ click_button "Compare branches and continue"
+
+ fill_in "merge_request_title", with: 'My brand new feature'
+ fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
+ click_button "Submit merge request"
+
+ merge_request = another_project.merge_requests.first
+ expect(merge_request.description).to eq "le feature \nFeature description:"
+ expect(merge_request.target_branch).to eq 'fix'
+ end
+
+ it 'does not change target branch when merge request is edited' do
+ new_merge_request = create(:merge_request, source_project: another_project)
+
+ visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request)
+ fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
+ click_button "Save changes"
+
+ new_merge_request = another_project.merge_requests.first
+ expect(new_merge_request.description).to include('/target_branch')
+ expect(new_merge_request.target_branch).not_to eq('fix')
+ end
+ end
+
+ describe '/target_branch command from note' do
+ context 'when the current user can change target branch' do
+ it 'changes target branch from a note' do
+ write_note("message start \n/target_branch merge-test\n message end.")
+
+ expect(page).not_to have_content('/target_branch')
+ expect(page).to have_content('message start')
+ expect(page).to have_content('message end.')
+
+ expect(merge_request.reload.target_branch).to eq 'merge-test'
+ end
+
+ it 'does not fail when target branch does not exists' do
+ write_note('/target_branch totally_not_existing_branch')
+
+ expect(page).not_to have_content('/target_branch')
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+
+ context 'when current user can not change target branch' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not change target branch' do
+ write_note('/target_branch merge-test')
+
+ expect(page).not_to have_content '/target_branch merge-test'
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+ end
end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
new file mode 100644
index 00000000000..957e913bf95
--- /dev/null
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+describe 'Merge request', :feature, :js do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ )
+ end
+
+ it 'shows widget status after creating new merge request' do
+ click_button 'Submit merge request'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.accept_merge_request')
+ end
+ end
+
+ context 'view merge request' do
+ let!(:environment) { create(:environment, project: project) }
+ let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) }
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows environments link' do
+ wait_for_ajax
+
+ page.within('.mr-widget-heading') do
+ expect(page).to have_content("Deployed to #{environment.name}")
+ expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
+ end
+ end
+ end
+end
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index aadd72a9f8e..8de9942c54e 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Milestone draggable', feature: true, js: true do
include WaitForAjax
+ include DragTo
let(:milestone) { create(:milestone, project: project, title: 8.14) }
let(:project) { create(:empty_project, :public) }
@@ -75,7 +76,7 @@ describe 'Milestone draggable', feature: true, js: true do
create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
visit namespace_project_milestone_path(project.namespace, project, milestone)
- issue.drag_to(issue_target)
+ drag_to(selector: '.issues-sortable-list', list_to_index: 1)
wait_for_ajax
end
@@ -85,7 +86,7 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
- merge_request.drag_to(merge_request_target)
+ drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
wait_for_ajax
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index b785b2f7704..fab2d532e06 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -89,7 +89,7 @@ describe 'Comments', feature: true do
end
end
- it 'should reset the edit note form textarea with the original content of the note if cancelled' do
+ it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-cancel').click
@@ -198,7 +198,7 @@ describe 'Comments', feature: true do
end
describe 'the note form' do
- it "shouldn't add a second form for same row" do
+ it "does not add a second form for same row" do
click_diff_line
is_expected.
@@ -206,7 +206,7 @@ describe 'Comments', feature: true do
count: 1)
end
- it 'should be removed when canceled' do
+ it 'is removed when canceled' do
is_expected.to have_css('.js-temp-notes-holder')
page.within("form[data-line-code='#{line_code}']") do
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 55a01057c83..eb7b8a24669 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -34,7 +34,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
# Set date to 1st of next month
find_field("Expires at").trigger('focus')
- find("a[title='Next']").click
+ find(".pika-next").click
click_on "1"
# Scopes
diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb
new file mode 100644
index 00000000000..30e2d587267
--- /dev/null
+++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Blob shortcuts', feature: true do
+ include TreeHelper
+ let(:project) { create(:project, :public, :repository) }
+ let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
+ let(:sha) { project.repository.commit.sha }
+
+ describe 'On a file(blob)', js: true do
+ def get_absolute_url(path = "")
+ "http://#{page.server.host}:#{page.server.port}#{path}"
+ end
+
+ def visit_blob(fragment = nil)
+ visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
+ end
+
+ describe 'pressing "y"' do
+ it 'redirects to permalink with commit sha' do
+ visit_blob
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true)
+ end
+
+ it 'maintains fragment hash when redirecting' do
+ fragment = "L1"
+ visit_blob(fragment)
+
+ find('body').native.send_key('y')
+
+ expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 11d27feab0b..f7e0115643e 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -27,7 +27,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :pending)
end
- it "shows Pending tab builds" do
+ it "shows Pending tab jobs" do
expect(page).to have_link 'Cancel running'
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content build.short_sha
@@ -42,7 +42,7 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :running)
end
- it "shows Running tab builds" do
+ it "shows Running tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_link 'Cancel running'
expect(page).to have_content build.short_sha
@@ -57,20 +57,20 @@ feature 'Builds', :feature do
visit namespace_project_builds_path(project.namespace, project, scope: :finished)
end
- it "shows Finished tab builds" do
+ it "shows Finished tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
- expect(page).to have_content 'No builds to show'
+ expect(page).to have_content 'No jobs to show'
expect(page).to have_link 'Cancel running'
end
end
- context "All builds" do
+ context "All jobs" do
before do
project.builds.running_or_pending.each(&:success)
visit namespace_project_builds_path(project.namespace, project)
end
- it "shows All tab builds" do
+ it "shows All tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content build.short_sha
expect(page).to have_content build.ref
@@ -98,7 +98,7 @@ feature 'Builds', :feature do
end
describe "GET /:project/builds/:id" do
- context "Build from project" do
+ context "Job from project" do
before do
visit namespace_project_build_path(project.namespace, project, build)
end
@@ -111,7 +111,7 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
visit namespace_project_build_path(project.namespace, project, build2)
end
@@ -149,7 +149,7 @@ feature 'Builds', :feature do
context 'when expire date is defined' do
let(:expire_at) { Time.now + 7.days }
- context 'when user has ability to update build' do
+ context 'when user has ability to update job' do
it 'keeps artifacts when keep button is clicked' do
expect(page).to have_content 'The artifacts will be removed'
@@ -160,7 +160,7 @@ feature 'Builds', :feature do
end
end
- context 'when user does not have ability to update build' do
+ context 'when user does not have ability to update job' do
let(:user_access_level) { :guest }
it 'does not have keep button' do
@@ -197,8 +197,8 @@ feature 'Builds', :feature do
visit namespace_project_build_path(project.namespace, project, build)
end
- context 'when build has an initial trace' do
- it 'loads build trace' do
+ context 'when job has an initial trace' do
+ it 'loads job trace' do
expect(page).to have_content 'BUILD TRACE'
build.append_trace(' and more trace', 11)
@@ -242,32 +242,32 @@ feature 'Builds', :feature do
end
end
- context 'when build starts environment' do
+ context 'when job starts environment' do
let(:environment) { create(:environment, project: project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
- context 'build is successfull and has deployment' do
+ context 'job is successfull and has deployment' do
let(:deployment) { create(:deployment) }
let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build is complete and not successfull' do
+ context 'job is complete and not successfull' do
let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) }
- it 'shows a link for the build' do
+ it 'shows a link for the job' do
visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link environment.name
end
end
- context 'build creates a new deployment' do
+ context 'job creates a new deployment' do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
@@ -281,7 +281,7 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/cancel" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -295,7 +295,7 @@ feature 'Builds', :feature do
end
end
- context "Build from other project" do
+ context "Job from other project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
@@ -307,13 +307,13 @@ feature 'Builds', :feature do
end
describe "POST /:project/builds/:id/retry" do
- context "Build from project" do
+ context "Job from project" do
before do
build.run!
visit namespace_project_build_path(project.namespace, project, build)
click_link 'Cancel'
page.within('.build-header') do
- click_link 'Retry build'
+ click_link 'Retry job'
end
end
diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb
index 33f1c323af1..268d420c594 100644
--- a/spec/features/projects/commit/builds_spec.rb
+++ b/spec/features/projects/commit/builds_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project commit pipelines' do
+feature 'project commit pipelines', js: true do
given(:project) { create(:project) }
background do
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 7baf7913424..7baf7913424 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
diff --git a/spec/features/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..43eb4000e58 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
index fe047e00409..36a80d7575d 100644
--- a/spec/features/projects/files/editing_a_file_spec.rb
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do
let(:user) { create(:user) }
let(:commit_params) do
{
- source_branch: project.default_branch,
+ start_branch: project.default_branch,
target_branch: project.default_branch,
commit_message: "Committing First Update",
file_path: ".gitignore",
diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb
index fc88fd74af8..582349d8d5b 100644
--- a/spec/features/projects/files/find_file_keyboard_spec.rb
+++ b/spec/features/projects/files/find_file_keyboard_spec.rb
@@ -22,7 +22,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do
expect(page).to have_selector('.blob-content-holder')
- page.within('.file-title') do
+ page.within('.js-file-title') do
expect(page).to have_content('CHANGELOG')
end
end
@@ -35,7 +35,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do
expect(page).to have_selector('.blob-content-holder')
- page.within('.file-title') do
+ page.within('.js-file-title') do
expect(page).to have_content('application.js')
end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index a521ce50f35..64094af29c0 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -6,7 +6,8 @@ feature 'project owner creates a license file', feature: true, js: true do
let(:project_master) { create(:user) }
let(:project) { create(:project) }
background do
- project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master')
+ project.repository.remove_file(project_master, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
project.team << [project_master, :master]
login_as(project_master)
visit namespace_project_path(project.namespace, project)
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 52d08982c7a..16dddb2a86b 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -74,6 +74,9 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
correspondent hash or model as the value.
+ Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS if it needs to be
+ reset (to prevent duplicate column problems while importing to the same instance).
+
IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
CURRENT_SPEC: #{__FILE__}
MSG
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 7655c2b351f..20cdfbae24f 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 6dae5c64b30..e90a033b8c4 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -18,8 +18,20 @@ feature 'issuable templates', feature: true, js: true do
let(:description_addition) { ' appending to description' }
background do
- project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
- project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/test.md',
+ longtemplate_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -67,7 +79,13 @@ feature 'issuable templates', feature: true, js: true do
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
- project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/issue_templates/bug.md',
+ template_content,
+ message: 'added issue template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
fill_in :'issue[description]', with: prior_description
@@ -86,7 +104,13 @@ feature 'issuable templates', feature: true, js: true do
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
background do
- project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.commit_file(
+ user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
@@ -111,7 +135,13 @@ feature 'issuable templates', feature: true, js: true do
fork_project.team << [fork_user, :master]
create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
login_as fork_user
- project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ project.repository.commit_file(
+ fork_user,
+ '.gitlab/merge_request_templates/feature-proposal.md',
+ template_content,
+ message: 'added merge request template',
+ branch_name: 'master',
+ update: false)
visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
fill_in :'merge_request[title]', with: 'test merge request title'
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index c9fa8315e79..1e900d7e660 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
feature 'Prioritize labels', feature: true do
include WaitForAjax
+ include DragTo
let(:user) { create(:user) }
let(:group) { create(:group) }
@@ -20,7 +21,7 @@ feature 'Prioritize labels', feature: true do
scenario 'user can prioritize a group label', js: true do
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('No prioritized labels yet')
+ expect(page).to have_content('Star labels to start sorting by priority')
page.within('.other-labels') do
all('.js-toggle-priority')[1].click
@@ -29,7 +30,7 @@ feature 'Prioritize labels', feature: true do
end
page.within('.prioritized-labels') do
- expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).not_to have_content('Star labels to start sorting by priority')
expect(page).to have_content('feature')
end
end
@@ -55,7 +56,7 @@ feature 'Prioritize labels', feature: true do
scenario 'user can prioritize a project label', js: true do
visit namespace_project_labels_path(project.namespace, project)
- expect(page).to have_content('No prioritized labels yet')
+ expect(page).to have_content('Star labels to start sorting by priority')
page.within('.other-labels') do
first('.js-toggle-priority').click
@@ -64,7 +65,7 @@ feature 'Prioritize labels', feature: true do
end
page.within('.prioritized-labels') do
- expect(page).not_to have_content('No prioritized labels yet')
+ expect(page).not_to have_content('Star labels to start sorting by priority')
expect(page).to have_content('bug')
end
end
@@ -99,7 +100,7 @@ feature 'Prioritize labels', feature: true do
expect(page).to have_content 'wontfix'
# Sort labels
- find("#project_label_#{bug.id}").drag_to find("#group_label_#{feature.id}")
+ drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
expect(first('li')).to have_content('feature')
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index f136d9ce0fa..c3f45be6e4b 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -14,15 +14,16 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
- scenario 'expiration date is displayed in the members list', js: true do
+ scenario 'expiration date is displayed in the members list' do
travel_to Time.zone.parse('2016-08-06 08:00') do
- visit namespace_project_settings_members_path(project.namespace, project)
+ date = 4.days.from_now
+ visit namespace_project_project_members_path(project.namespace, project)
+
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
- fill_in 'expires_at', with: '2016-08-10'
+ fill_in 'expires_at', with: date.to_s(:medium)
+ click_on 'Add to project'
end
- find('.users-project-form').click
- click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
@@ -32,11 +33,12 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
scenario 'change expiration date' do
travel_to Time.zone.parse('2016-08-06 08:00') do
- project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ date = 3.days.from_now
+ project.team.add_users([new_member.id], :developer, expires_at: Date.today.to_s(:medium))
visit namespace_project_project_members_path(project.namespace, project)
page.within "#project_member_#{new_member.project_members.first.id}" do
- find('.js-access-expiration-date').set '2016-08-09'
+ find('.js-access-expiration-date').set date.to_s(:medium)
wait_for_ajax
expect(page).to have_content('Expires in 3 days')
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index abfc46601fb..b56e562b2b6 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,11 +1,13 @@
require "spec_helper"
feature "New project", feature: true do
- context "Visibility level selector" do
- let(:user) { create(:admin) }
+ let(:user) { create(:admin) }
- before { login_as(user) }
+ before do
+ login_as(user)
+ end
+ context "Visibility level selector" do
Gitlab::VisibilityLevel.options.each do |key, level|
it "sets selector to #{key}" do
stub_application_setting(default_project_visibility: level)
@@ -16,4 +18,16 @@ feature "New project", feature: true do
end
end
end
+
+ context 'Import project options' do
+ before do
+ visit new_project_path
+ end
+
+ it 'does not autocomplete sensitive git repo URL' do
+ autocomplete = find('#project_import_url')['autocomplete']
+
+ expect(autocomplete).to eq('off')
+ end
+ end
end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
new file mode 100644
index 00000000000..11793c0f303
--- /dev/null
+++ b/spec/features/projects/pages_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+feature 'Pages', feature: true do
+ given(:project) { create(:empty_project) }
+ given(:user) { create(:user) }
+ given(:role) { :master }
+
+ background do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+
+ project.team << [user, role]
+
+ login_as(user)
+ end
+
+ shared_examples 'no pages deployed' do
+ scenario 'does not see anything to destroy' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).not_to have_link('Remove pages')
+ expect(page).not_to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ context 'when user is the owner' do
+ background do
+ project.namespace.update(owner: user)
+ end
+
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Remove pages" link' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_link('Remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+
+ context 'when the user is not the owner' do
+ context 'when pages deployed' do
+ background do
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'sees "Only the project owner can remove pages" text' do
+ visit namespace_project_pages_path(project.namespace, project)
+
+ expect(page).to have_text('Only the project owner can remove pages')
+ end
+ end
+
+ it_behaves_like 'no pages deployed'
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index e673ece37c3..0b5ccc8c515 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -66,8 +66,8 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
- expect(page).to have_selector('.ci-status-icon-running')
- expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+ expect(page).to have_selector('.js-ci-status-icon-running')
+ expect(page).to have_selector('.js-icon-action-cancel')
expect(page).to have_content('deploy')
end
end
@@ -82,63 +82,63 @@ describe 'Pipeline', :feature, :js do
context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('build')
end
page.within('#ci-badge-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
- it 'should be possible to retry the success build' do
+ it 'should be possible to retry the success job' do
find('#ci-badge-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
context 'when pipeline has failed builds' do
it 'shows the failed icon and a retry action for the failed build' do
page.within('#ci-badge-test') do
- expect(page).to have_selector('.ci-status-icon-failed')
+ expect(page).to have_selector('.js-ci-status-icon-failed')
expect(page).to have_content('test')
end
page.within('#ci-badge-test .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+ expect(page).to have_selector('.js-icon-action-retry')
end
end
it 'should be possible to retry the failed build' do
find('#ci-badge-test .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Retry build')
+ expect(page).not_to have_content('Retry job')
end
end
- context 'when pipeline has manual builds' do
+ context 'when pipeline has manual jobs' do
it 'shows the skipped icon and a play action for the manual build' do
page.within('#ci-badge-manual-build') do
- expect(page).to have_selector('.ci-status-icon-manual')
+ expect(page).to have_selector('.js-ci-status-icon-manual')
expect(page).to have_content('manual')
end
page.within('#ci-badge-manual-build .ci-action-icon-container') do
- expect(page).to have_selector('.ci-action-icon-container .fa-play')
+ expect(page).to have_selector('.js-icon-action-play')
end
end
- it 'should be possible to play the manual build' do
+ it 'should be possible to play the manual job' do
find('#ci-badge-manual-build .ci-action-icon-container').trigger('click')
- expect(page).not_to have_content('Play build')
+ expect(page).not_to have_content('Play job')
end
end
- context 'when pipeline has external build' do
+ context 'when pipeline has external job' do
it 'shows the success icon and the generic comit status build' do
- expect(page).to have_selector('.ci-status-icon-success')
+ expect(page).to have_selector('.js-ci-status-icon-success')
expect(page).to have_content('jenkins')
expect(page).to have_link('jenkins', href: 'http://gitlab.com/status')
end
@@ -146,12 +146,12 @@ describe 'Pipeline', :feature, :js do
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
@@ -160,7 +160,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
@@ -170,7 +170,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -191,7 +191,7 @@ describe 'Pipeline', :feature, :js do
visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)
end
- it 'shows a list of builds' do
+ it 'shows a list of jobs' do
expect(page).to have_content('Test')
expect(page).to have_content(build_passed.id)
expect(page).to have_content('Deploy')
@@ -203,26 +203,26 @@ describe 'Pipeline', :feature, :js do
expect(page).to have_link('Play')
end
- it 'shows Builds tab pane as active' do
+ it 'shows jobs tab pane as active' do
expect(page).to have_css('#js-tab-builds.active')
end
context 'page tabs' do
- it 'shows Pipeline and Builds tabs with link' do
+ it 'shows Pipeline and Jobs tabs with link' do
expect(page).to have_link('Pipeline')
- expect(page).to have_link('Builds')
+ expect(page).to have_link('Jobs')
end
- it 'shows counter in Builds tab' do
+ it 'shows counter in Jobs tab' do
expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s)
end
- it 'shows Builds tab as active' do
+ it 'shows Jobs tab as active' do
expect(page).to have_css('li.js-builds-tab-link.active')
end
end
- context 'retrying builds' do
+ context 'retrying jobs' do
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
@@ -233,7 +233,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'canceling builds' do
+ context 'canceling jobs' do
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -244,7 +244,7 @@ describe 'Pipeline', :feature, :js do
end
end
- context 'playing manual build' do
+ context 'playing manual job' do
before do
within '.pipeline-holder' do
click_link('Play')
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ca18ac073d8..6555b2fc6c1 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -35,6 +35,10 @@ describe 'Pipelines', :feature, :js do
it 'contains pipeline commit short SHA' do
expect(page).to have_content(pipeline.short_sha)
end
+
+ it 'contains branch name' do
+ expect(page).to have_content(pipeline.ref)
+ end
end
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
index 472491188c9..38fe2d92885 100644
--- a/spec/features/projects/ref_switcher_spec.rb
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -17,14 +17,15 @@ feature 'Ref switcher', feature: true, js: true do
page.within '.project-refs-form' do
input = find('input[type="search"]')
- input.set 'expand'
+ input.set 'binary'
+ wait_for_ajax
input.native.send_keys :down
input.native.send_keys :down
input.native.send_keys :enter
end
- expect(page).to have_title 'expand-collapse-files'
+ expect(page).to have_title 'binary-encoding'
end
it "user selects ref with special characters" do
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 86a07b2c679..f5adb53a2dc 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Setup Mattermost slash commands', feature: true do
- include WaitForAjax
-
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
end
- describe 'user visits the mattermost slash command config page', js: true do
+ describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do
- wait_for_ajax
+ expect(page).to have_content("This service allows users to perform common")
+ end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content("This service allows GitLab users to perform common")
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
it 'shows the token after saving' do
@@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(true)
+ expect(select_element['disabled']).to eq('disabled')
expect(selected_option).to have_content(team_name.to_s)
end
@@ -93,12 +95,21 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
- expect(select_element['disabled']).to be(false)
+ expect(select_element['disabled']).to be(nil)
expect(selected_option).to have_content('Select team...')
# The 'Select team...' placeholder is item `0`.
expect(select_element.all('option').count).to eq(3)
end
+ it 'shows an error alert with the error message if there is an error requesting teams' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
+
+ click_link 'Add to Mattermost'
+
+ expect(page).to have_selector('.alert')
+ expect(page).to have_content('test mattermost error message')
+ end
+
def stub_teams(count: 0)
teams = create_teams(count)
@@ -126,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end
+
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
+
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
end
end
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index 32b32f7ae8e..db903a0c8f0 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'Slack slash commands', feature: true do
- include WaitForAjax
-
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do
project.team << [user, :master]
login_as(user)
- end
-
- scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
+ end
- wait_for_ajax
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- expect(page).to have_content('This service allows GitLab users to perform common')
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- scenario 'shows the token after saving' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
+ it 'shows a help message' do
+ expect(page).to have_content('This service allows users to perform common')
+ end
+ it 'shows the token after saving' do
fill_in 'service_token', with: 'token'
click_on 'Save'
@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token')
end
- scenario 'shows the correct trigger url' do
- visit edit_namespace_project_service_path(project.namespace, project, service)
-
+ it 'shows the correct trigger url' do
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 4bfaa499272..6815039d5ed 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -11,41 +11,36 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
login_as(user)
end
- context 'when Merge Request and Builds are initially enabled' do
- before do
- project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED)
- end
-
- context 'when Builds are initially enabled' do
+ context 'when Merge Request and Pipelines are initially enabled' do
+ context 'when Pipelines are initially enabled' do
before do
- project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings' do
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
- context 'when Builds are initially disabled' do
+ context 'when Pipelines are initially disabled' do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
end
scenario 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
@@ -58,12 +53,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
end
scenario 'does not show the Merge Requests settings' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
- expect(page).to have_content('Only allow merge requests to be merged if the build succeeds')
+ expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb
new file mode 100644
index 00000000000..ce5c5f21167
--- /dev/null
+++ b/spec/features/projects/view_on_env_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe 'View on environment', js: true do
+ include WaitForAjax
+
+ let(:branch_name) { 'feature' }
+ let(:file_path) { 'files/ruby/feature.rb' }
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'when the branch has a route map' do
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /files/(.*)\\..*/
+ public: '\\1'
+ MAP
+ end
+
+ before do
+ Files::CreateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Add .gitlab/route-map.yml",
+ file_path: '.gitlab/route-map.yml',
+ file_content: route_map
+ ).execute
+
+ # Update the file so that we still have a commit that will have a file on the environment
+ Files::UpdateService.new(
+ project,
+ user,
+ start_branch: branch_name,
+ target_branch: branch_name,
+ commit_message: "Update feature",
+ file_path: file_path,
+ file_content: "# Noop"
+ ).execute
+ end
+
+ context 'and an active deployment' do
+ let(:sha) { project.commit(branch_name).sha }
+ let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') }
+ let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) }
+
+ context 'when visiting the diff of a merge request for the branch' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) }
+
+ before do
+ login_as(user)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ within '.diffs' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+
+ context 'when visiting a comparison for the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a comparison for the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the branch' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting a blob on the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path))
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+
+ context 'when visiting the commit' do
+ before do
+ login_as(user)
+
+ visit namespace_project_commit_path(project.namespace, project, sha)
+
+ wait_for_ajax
+ end
+
+ it 'has a "View on env" button' do
+ expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index b4f5f6b3fc5..20219f3cc9a 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe 'Projects > Wiki > User views wiki in project page', feature: true do
let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
before do
project.team << [user, :master]
@@ -10,12 +9,11 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do
end
context 'when repository is disabled for project' do
- before do
- project.project_feature.update!(
- repository_access_level: ProjectFeature::DISABLED,
- merge_requests_access_level: ProjectFeature::DISABLED,
- builds_access_level: ProjectFeature::DISABLED
- )
+ let(:project) do
+ create(:empty_project,
+ :repository_disabled,
+ :merge_requests_disabled,
+ :builds_disabled)
end
context 'when wiki homepage contains a link' do
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
index 395c61a4743..e4aca25a339 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -26,7 +26,11 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-push").click
- within('.js-allowed-to-push-container') { click_on access_type_name }
+
+ within('.js-allowed-to-push-container') do
+ expect(first("li")).to have_content("Roles")
+ click_on access_type_name
+ end
end
wait_for_ajax
@@ -61,7 +65,11 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".protected-branches-list") do
find(".js-allowed-to-merge").click
- within('.js-allowed-to-merge-container') { click_on access_type_name }
+
+ within('.js-allowed-to-merge-container') do
+ expect(first("li")).to have_content("Roles")
+ click_on access_type_name
+ end
end
wait_for_ajax
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 92d5a2fbc48..24af062d763 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -96,6 +96,20 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index b616e488487..c511dcfa18e 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -92,8 +92,22 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_allowed_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
it { is_expected.to be_denied_for(:external) }
+ end
+
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
end
describe "GET /:project_path/blob" do
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index ded85e837f4..d8cc012c27e 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -96,6 +96,20 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:external) }
end
+ describe "GET /:project_path/settings/ci_cd" do
+ subject { namespace_project_settings_ci_cd_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
diff --git a/spec/features/snippets/user_snippets_spec.rb b/spec/features/snippets/user_snippets_spec.rb
new file mode 100644
index 00000000000..191c2fb9a22
--- /dev/null
+++ b/spec/features/snippets/user_snippets_spec.rb
@@ -0,0 +1,49 @@
+require 'rails_helper'
+
+feature 'User Snippets', feature: true do
+ let(:author) { create(:user) }
+ let!(:public_snippet) { create(:personal_snippet, :public, author: author, title: "This is a public snippet") }
+ let!(:internal_snippet) { create(:personal_snippet, :internal, author: author, title: "This is an internal snippet") }
+ let!(:private_snippet) { create(:personal_snippet, :private, author: author, title: "This is a private snippet") }
+
+ background do
+ login_as author
+ visit dashboard_snippets_path
+ end
+
+ scenario 'View all of my snippets' do
+ expect(page).to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).to have_content(private_snippet.title)
+ end
+
+ scenario 'View my public snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Public"
+ end
+
+ expect(page).to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'View my internal snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Internal"
+ end
+
+ expect(page).not_to have_content(public_snippet.title)
+ expect(page).to have_content(internal_snippet.title)
+ expect(page).not_to have_content(private_snippet.title)
+ end
+
+ scenario 'View my private snippets' do
+ page.within('.snippet-scope-menu') do
+ click_link "Private"
+ end
+
+ expect(page).not_to have_content(public_snippet.title)
+ expect(page).not_to have_content(internal_snippet.title)
+ expect(page).to have_content(private_snippet.title)
+ end
+end
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index d1f2bc78884..e8f06916d53 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -98,15 +98,58 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
expect(find('.todos-list')).not_to have_content merge_request.to_reference
end
- it 'filters by action' do
- click_button 'Action'
- within '.dropdown-menu-action' do
- click_link 'Assigned'
+ describe 'filter by action' do
+ before do
+ create(:todo, :build_failed, user: user_1, author: user_2, project: project_1)
+ create(:todo, :marked, user: user_1, author: user_2, project: project_1, target: issue)
end
- wait_for_ajax
+ it 'filters by Assigned' do
+ filter_action('Assigned')
+
+ expect_to_see_action(:assigned)
+ end
+
+ it 'filters by Mentioned' do
+ filter_action('Mentioned')
+
+ expect_to_see_action(:mentioned)
+ end
+
+ it 'filters by Added' do
+ filter_action('Added')
+
+ expect_to_see_action(:marked)
+ end
+
+ it 'filters by Pipelines' do
+ filter_action('Pipelines')
- expect(find('.todos-list')).to have_content ' assigned you '
- expect(find('.todos-list')).not_to have_content ' mentioned '
+ expect_to_see_action(:build_failed)
+ end
+
+ def filter_action(name)
+ click_button 'Action'
+ within '.dropdown-menu-action' do
+ click_link name
+ end
+
+ wait_for_ajax
+ end
+
+ def expect_to_see_action(action_name)
+ action_names = {
+ assigned: ' assigned you ',
+ mentioned: ' mentioned ',
+ marked: ' added a todo for ',
+ build_failed: ' build failed for '
+ }
+
+ action_name_text = action_names.delete(action_name)
+ expect(find('.todos-list')).to have_content action_name_text
+ action_names.each_value do |other_action_text|
+ expect(find('.todos-list')).not_to have_content other_action_text
+ end
+ end
end
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 3850e930b6d..1b352be9331 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -171,7 +171,7 @@ describe 'Dashboard Todos', feature: true do
it 'links to the pipelines for the merge request' do
href = pipelines_namespace_project_merge_request_path(project.namespace, project, todo.target)
- expect(page).to have_link "merge request #{todo.target.to_reference}", href
+ expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href
end
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 72354834c5a..4a7511589d6 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -7,7 +7,7 @@ describe 'Triggers' do
before do
@project = FactoryGirl.create :empty_project
@project.team << [user, :master]
- visit namespace_project_triggers_path(@project.namespace, @project)
+ visit namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
context 'create a trigger' do
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index ff30ffd7820..9a4bc027004 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -10,7 +10,7 @@ describe 'Project variables', js: true do
project.team << [user, :master]
project.variables << variable
- visit namespace_project_variables_path(project.namespace, project)
+ visit namespace_project_settings_ci_cd_path(project.namespace, project)
end
it 'shows list of variables' do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index ad2d456529a..34f665826b6 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -10,15 +10,12 @@ describe ContributedProjectsFinder do
let!(:private_project) { create(:empty_project, :private) }
before do
- private_project.team << [source_user, Gitlab::Access::MASTER]
- private_project.team << [current_user, Gitlab::Access::DEVELOPER]
- public_project.team << [source_user, Gitlab::Access::MASTER]
+ private_project.add_master(source_user)
+ private_project.add_developer(current_user)
+ public_project.add_master(source_user)
- create(:event, action: Event::PUSHED, project: public_project,
- target: public_project, author: source_user)
-
- create(:event, action: Event::PUSHED, project: private_project,
- target: private_project, author: source_user)
+ create(:event, :pushed, project: public_project, target: public_project, author: source_user)
+ create(:event, :pushed, project: private_project, target: private_project, author: source_user)
end
describe 'without a current user' do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
new file mode 100644
index 00000000000..0c063f6d5ee
--- /dev/null
+++ b/spec/finders/environments_finder_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe EnvironmentsFinder do
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+ let(:environment) { create(:environment, project: project) }
+
+ before do
+ project.add_master(user)
+ end
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute)
+ .to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute)
+ .to be_empty
+ end
+
+ it 'returns environment when commit constraint is not set' do
+ expect(described_class.new(project, user, ref: 'master').execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'commit deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment' do
+ expect(described_class.new(project, user, commit: project.commit).execute)
+ .to contain_exactly(environment)
+ end
+ end
+
+ context 'recently updated' do
+ 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(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .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(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .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(described_class.new(project, user, ref: 'feature', recently_updated: true).execute)
+ .to contain_exactly(environment, second_environment)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
new file mode 100644
index 00000000000..b762756f9ce
--- /dev/null
+++ b/spec/finders/group_members_finder_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe GroupMembersFinder, '#execute' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, :access_requestable, parent: group) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:user4) { create(:user) }
+
+ it 'returns members for top-level group' do
+ member1 = group.add_master(user1)
+ member2 = group.add_master(user2)
+ member3 = group.add_master(user3)
+
+ result = described_class.new(group).execute
+
+ expect(result.to_a).to eq([member3, member2, member1])
+ end
+
+ it 'returns members for nested group' do
+ group.add_master(user2)
+ nested_group.request_access(user4)
+ member1 = group.add_master(user1)
+ member3 = nested_group.add_master(user2)
+ member4 = nested_group.add_master(user3)
+
+ result = described_class.new(nested_group).execute
+
+ expect(result.to_a).to eq([member4, member3, member1])
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 97737d7ddc7..12ab1d6dde8 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -224,7 +224,7 @@ describe IssuesFinder do
let(:scope) { nil }
it "doesn't return team-only issues to non team members" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
issue = create(:issue, project: project)
expect(issues).not_to include(issue)
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index bac653ea451..f8b05d4e9bc 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -9,8 +9,6 @@ describe NotesFinder do
end
describe '#execute' do
- it 'finds notes on snippets when project is public and user isnt a member'
-
it 'finds notes on merge requests' do
create(:note_on_merge_request, project: project)
@@ -45,9 +43,11 @@ describe NotesFinder do
context 'on restricted projects' do
let(:project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE,
- snippets_access_level: ProjectFeature::PRIVATE,
- merge_requests_access_level: ProjectFeature::PRIVATE)
+ create(:empty_project,
+ :public,
+ :issues_private,
+ :snippets_private,
+ :merge_requests_private)
end
it 'publicly excludes notes on merge requests' do
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 77f2bcee1f3..8e19cee5440 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -6,6 +6,7 @@
"confidential"
],
"properties" : {
+ "id": { "type": "integer" },
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 8d94cf26ecb..819287bf919 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -10,7 +10,7 @@
"id": { "type": "integer" },
"list_type": {
"type": "string",
- "enum": ["backlog", "label", "done"]
+ "enum": ["label", "done"]
},
"label": {
"type": ["object", "null"],
diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
new file mode 100644
index 00000000000..6823db0cfc8
--- /dev/null
+++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml
@@ -0,0 +1,42 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>,<exchange@microsoft.com>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+I could not disagree more. I am obviously biased but adventure time is the
+greatest show ever created. Everyone should watch it.
+
+- Jake out
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz
new file mode 100644
index 00000000000..d0e89378b3e
--- /dev/null
+++ b/spec/fixtures/pages.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip
new file mode 100644
index 00000000000..9558fcd4b94
--- /dev/null
+++ b/spec/fixtures/pages.zip
Binary files differ
diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta
new file mode 100644
index 00000000000..1e6198a15f0
--- /dev/null
+++ b/spec/fixtures/pages.zip.meta
Binary files differ
diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz
new file mode 100644
index 00000000000..5c2afa1a8f6
--- /dev/null
+++ b/spec/fixtures/pages_empty.tar.gz
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip
new file mode 100644
index 00000000000..db3f0334c12
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip
Binary files differ
diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta
new file mode 100644
index 00000000000..d0b93b3b9c0
--- /dev/null
+++ b/spec/fixtures/pages_empty.zip.meta
Binary files differ
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 727c25ff529..a2c008790f9 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -26,4 +26,23 @@ describe CommitsHelper do
not_to include('onmouseover="alert(1)"')
end
end
+
+ describe '#view_on_environment_button' do
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, external_url: 'http://example.com') }
+ let(:path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html')
+ end
+
+ it 'returns a link tag linking to the file in the environment' do
+ html = helper.view_on_environment_button(sha, path, environment)
+ node = Nokogiri::HTML.parse(html).at_css('a')
+
+ expect(node[:title]).to eq('View on example.com')
+ expect(node[:href]).to eq('http://example.com/file.html')
+ end
+ end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 468bcc7badc..eae097126ce 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -134,7 +134,7 @@ describe DiffHelper do
let(:new_pos) { 50 }
let(:text) { 'some_text' }
- it "should generate foldable top match line for inline view with empty text by default" do
+ it "generates foldable top match line for inline view with empty text by default" do
output = diff_match_line old_pos, new_pos
expect(output).to be_html_safe
@@ -143,7 +143,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
end
- it "should allow to define text and bottom option" do
+ it "allows to define text and bottom option" do
output = diff_match_line old_pos, new_pos, text: text, bottom: true
expect(output).to be_html_safe
@@ -152,7 +152,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
end
- it "should generate match line for parallel view" do
+ it "generates match line for parallel view" do
output = diff_match_line old_pos, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
@@ -162,7 +162,7 @@ describe DiffHelper do
expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
end
- it "should allow to generate only left match line for parallel view" do
+ it "allows to generate only left match line for parallel view" do
output = diff_match_line old_pos, nil, text: text, view: :parallel
expect(output).to be_html_safe
@@ -171,7 +171,7 @@ describe DiffHelper do
expect(output).not_to have_css 'td:nth-child(3)'
end
- it "should allow to generate only right match line for parallel view" do
+ it "allows to generate only right match line for parallel view" do
output = diff_match_line nil, new_pos, text: text, view: :parallel
expect(output).to be_html_safe
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index a4f08dc4af0..df71680e44c 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -115,6 +115,46 @@ describe IssuablesHelper do
end
end
+ describe '#issuable_reference' do
+ context 'when show_full_reference truthy' do
+ it 'display issuable full reference' do
+ assign(:show_full_reference, true)
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(full: true))
+ end
+ end
+
+ context 'when show_full_reference falsey' do
+ context 'when @group present' do
+ it 'display issuable reference to @group' do
+ project = build_stubbed(:project)
+
+ assign(:show_full_reference, nil)
+ assign(:group, project.namespace)
+
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project.namespace))
+ end
+ end
+
+ context 'when @project present' do
+ it 'display issuable reference to @project' do
+ project = build_stubbed(:project)
+
+ assign(:show_full_reference, nil)
+ assign(:group, nil)
+ assign(:project, project)
+
+ issue = build_stubbed(:issue)
+
+ expect(helper.issuable_reference(issue)).to eql(issue.to_reference(project))
+ end
+ end
+ end
+ end
+
describe '#issuable_filter_present?' do
it 'returns true when any key is present' do
allow(helper).to receive(:params).and_return(
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 550b4a66a6a..25f23826648 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -63,9 +63,11 @@ describe MergeRequestsHelper do
end
end
- describe 'mr_widget_refresh_url' do
- let(:project) { create(:empty_project) }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ describe '#mr_widget_refresh_url' do
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:project_fork) { Projects::ForkService.new(project, guest).execute }
+ let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
it 'returns correct url for MR' do
expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
@@ -74,7 +76,89 @@ describe MergeRequestsHelper do
end
it 'returns empty string for nil' do
- expect(mr_widget_refresh_url(nil)).to end_with('')
+ expect(mr_widget_refresh_url(nil)).to eq('')
+ end
+ end
+
+ describe '#mr_closes_issues' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_closes_issues).to contain_exactly(issue_1, issue_2)
+ end
+ end
+ end
+
+ describe '#mr_issues_mentioned_but_not_closing' do
+ let(:user_1) { create(:user) }
+ let(:user_2) { create(:user) }
+
+ let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
+ let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
+
+ let(:issue_1) { create(:issue, project: project_1) }
+ let(:issue_2) { create(:issue, project: project_2) }
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_1, target_project: project_1,
+ description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}")
+ end
+
+ before do
+ project_1.team << [user_2, :developer]
+ project_2.team << [user_2, :developer]
+ allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
+ @merge_request = merge_request
+ end
+
+ context 'user without access to another private project' do
+ let(:current_user) { user_1 }
+
+ it 'cannot see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1)
+ end
+ end
+
+ context 'user with access to another private project' do
+ let(:current_user) { user_2 }
+
+ it 'can see that project\'s issue that will be closed on acceptance' do
+ expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2)
+ end
end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 8d1570aa6f3..aca0bb1d794 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -203,7 +203,6 @@ describe ProjectsHelper do
context "when project moves from public to private" do
before do
- project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index e51720f10ed..b7e547dc1f5 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -41,6 +41,11 @@ describe SearchHelper do
expect(search_autocomplete_opts("gro").size).to eq(1)
end
+ it "includes nested group" do
+ create(:group, :nested, name: 'foo').add_owner(user)
+ expect(search_autocomplete_opts('foo').size).to eq(1)
+ end
+
it "includes the user's projects" do
project = create(:empty_project, namespace: create(:namespace, owner: user))
expect(search_autocomplete_opts(project.name).size).to eq(1)
diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc
index 3cd419b37c9..fbd9bb9f0ff 100644
--- a/spec/javascripts/.eslintrc
+++ b/spec/javascripts/.eslintrc
@@ -22,9 +22,10 @@
},
"plugins": ["jasmine"],
"rules": {
- "prefer-arrow-callback": 0,
"func-names": 0,
"jasmine/no-suite-dupes": [1, "branch"],
- "jasmine/no-spec-dupes": [1, "branch"]
+ "jasmine/no-spec-dupes": [1, "branch"],
+ "no-console": 0,
+ "prefer-arrow-callback": 0
}
}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index a2d57824585..76b370b345b 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,5 +1,5 @@
-/*= require lib/utils/text_utility */
-/*= require abuse_reports */
+require('~/lib/utils/text_utility');
+require('~/abuse_reports');
((global) => {
describe('Abuse Reports', () => {
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 7bc5b3268a0..e6a6fc36ca1 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,9 +1,8 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */
-/*= require js.cookie.js */
-/*= require jquery.endless-scroll.js */
-/*= require pager */
-/*= require activities */
+require('vendor/jquery.endless-scroll.js');
+require('~/pager');
+require('~/activities');
(() => {
window.gon || (window.gon = {});
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 71446b9df61..001cd8d6325 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,10 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
/* global AwardsHandler */
-/*= require awards_handler */
-/*= require jquery */
-/*= require js.cookie */
-/*= require ./fixtures/emoji_menu */
+require('~/awards_handler');
+require('./fixtures/emoji_menu');
(function() {
var awardsHandler, lazyAssert, urlRoot;
@@ -113,7 +111,7 @@
});
});
describe('::getAwardUrl', function() {
- return it('should return the url for request', function() {
+ return it('returns the url for request', function() {
return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
});
});
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 51d911792ba..4a3da9e318b 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */
-/*= require behaviors/autosize */
+require('~/behaviors/autosize');
(function() {
describe('Autosize behavior', function() {
@@ -15,7 +15,7 @@
});
});
return load = function() {
- return $(document).trigger('page:load');
+ return $(document).trigger('load');
};
});
}).call(this);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 0f046c2d83a..1541037888f 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */
-/*= require behaviors/quick_submit */
+require('~/behaviors/quick_submit');
(function() {
describe('Quick Submit behavior', function() {
@@ -40,7 +40,7 @@
expect($('input[type=submit]')).toBeDisabled();
return expect($('button[type=submit]')).toBeDisabled();
});
- // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
+ // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
// only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) {
it('responds to Meta+Enter', function() {
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 9467056f04c..a958ac76e66 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require behaviors/requires_input */
+require('~/behaviors/requires_input');
(function() {
describe('requiresInput', function() {
@@ -34,11 +34,5 @@
$('#required5').val('1').change();
return expect($('.submit')).not.toBeDisabled();
});
- return it('is called on page:load event', function() {
- var spy;
- spy = spyOn($.fn, 'requiresInput');
- $(document).trigger('page:load');
- return expect(spy).toHaveBeenCalled();
- });
});
}).call(this);
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 7c5850111cb..9dd741a680b 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -6,24 +6,19 @@
/* global listObj */
/* global listObjDuplicate */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+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.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
Cookies.set('issue_board_welcome_hidden', 'false', {
@@ -61,18 +56,6 @@ describe('Store', () => {
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');
-
- expect(list).toBeDefined();
- });
-
it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
const list = gl.issueBoards.BoardsStore.findList('id', 1);
@@ -117,10 +100,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
});
- it('check for blank state adding when backlog & done list exist', () => {
- gl.issueBoards.BoardsStore.addList({
- list_type: 'backlog'
- });
+ it('check for blank state adding when done list exist', () => {
gl.issueBoards.BoardsStore.addList({
list_type: 'done'
});
diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6
new file mode 100644
index 00000000000..4340a571017
--- /dev/null
+++ b/spec/javascripts/boards/issue_card_spec.js.es6
@@ -0,0 +1,191 @@
+/* global Vue */
+/* global ListUser */
+/* global ListLabel */
+/* global listObj */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/boards_store');
+require('~/boards/components/issue_card_inner');
+require('./mock_data');
+
+describe('Issue card component', () => {
+ const user = new ListUser({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: 'blue',
+ text_color: 'white',
+ description: 'test',
+ });
+ let component;
+ let issue;
+ let list;
+
+ beforeEach(() => {
+ setFixtures('<div class="test-container"></div>');
+
+ list = listObj;
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [list.label],
+ });
+
+ component = new Vue({
+ el: document.querySelector('.test-container'),
+ data() {
+ return {
+ list,
+ issue,
+ issueLinkBase: '/test',
+ rootPath: '/',
+ };
+ },
+ components: {
+ 'issue-card': gl.issueBoards.IssueCardInner,
+ },
+ template: `
+ <issue-card
+ :issue="issue"
+ :list="list"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"></issue-card>
+ `,
+ });
+ });
+
+ it('renders issue title', () => {
+ expect(
+ component.$el.querySelector('.card-title').textContent,
+ ).toContain(issue.title);
+ });
+
+ it('includes issue base in link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('href'),
+ ).toContain('/test');
+ });
+
+ it('includes issue title on link', () => {
+ expect(
+ component.$el.querySelector('.card-title a').getAttribute('title'),
+ ).toBe(issue.title);
+ });
+
+ it('does not render confidential icon', () => {
+ expect(
+ component.$el.querySelector('.fa-eye-flash'),
+ ).toBeNull();
+ });
+
+ it('renders confidential icon', (done) => {
+ component.issue.confidential = true;
+
+ setTimeout(() => {
+ expect(
+ component.$el.querySelector('.confidential-icon'),
+ ).not.toBeNull();
+ done();
+ }, 0);
+ });
+
+ it('renders issue ID with #', () => {
+ expect(
+ component.$el.querySelector('.card-number').textContent,
+ ).toContain(`#${issue.id}`);
+ });
+
+ describe('assignee', () => {
+ it('does not render assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.assignee = user;
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('renders assignee', () => {
+ expect(
+ component.$el.querySelector('.card-assignee'),
+ ).not.toBeNull();
+ });
+
+ it('sets title', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('title'),
+ ).toContain(`Assigned to ${user.name}`);
+ });
+
+ it('sets users path', () => {
+ expect(
+ component.$el.querySelector('.card-assignee').getAttribute('href'),
+ ).toBe('/test');
+ });
+
+ it('renders avatar', () => {
+ expect(
+ component.$el.querySelector('.card-assignee img'),
+ ).not.toBeNull();
+ });
+ });
+ });
+
+ describe('labels', () => {
+ it('does not render any', () => {
+ expect(
+ component.$el.querySelector('.label'),
+ ).toBeNull();
+ });
+
+ describe('exists', () => {
+ beforeEach((done) => {
+ component.issue.addLabel(label1);
+
+ setTimeout(() => {
+ done();
+ }, 0);
+ });
+
+ it('does not render list label', () => {
+ expect(
+ component.$el.querySelectorAll('.label').length,
+ ).toBe(1);
+ });
+
+ it('renders label', () => {
+ expect(
+ component.$el.querySelector('.label').textContent,
+ ).toContain(label1.title);
+ });
+
+ it('sets label description as title', () => {
+ expect(
+ component.$el.querySelector('.label').getAttribute('title'),
+ ).toContain(label1.description);
+ });
+
+ it('sets background color of button', () => {
+ expect(
+ component.$el.querySelector('.label').style.backgroundColor,
+ ).toContain(label1.color);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index c8a61a0a9b5..aab4d9c501e 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -2,25 +2,20 @@
/* global BoardService */
/* global ListIssue */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board', '1');
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index 7d942ec3d65..4397a32fedc 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -5,26 +5,21 @@
/* global List */
/* global listObj */
-//= require jquery
-//= require jquery_ujs
-//= require js.cookie
-//= require vue
-//= require vue-resource
-//= require lib/utils/url_utility
-//= require boards/models/issue
-//= require boards/models/label
-//= require boards/models/list
-//= require boards/models/user
-//= require boards/services/board_service
-//= require boards/stores/boards_store
-//= require ./mock_data
+require('~/lib/utils/url_utility');
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/services/board_service');
+require('~/boards/stores/boards_store');
+require('./mock_data');
describe('List model', () => {
let list;
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '1');
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 8d3e2237fda..7a399b307ad 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => {
status: 200
}));
};
+
+window.listObj = listObj;
+window.listObjDuplicate = listObjDuplicate;
+window.BoardsMockData = BoardsMockData;
+window.boardsMockInterceptor = boardsMockInterceptor;
diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6
new file mode 100644
index 00000000000..1815847f3fa
--- /dev/null
+++ b/spec/javascripts/boards/modal_store_spec.js.es6
@@ -0,0 +1,132 @@
+/* global Vue */
+/* global ListIssue */
+
+require('~/boards/models/issue');
+require('~/boards/models/label');
+require('~/boards/models/list');
+require('~/boards/models/user');
+require('~/boards/stores/modal_store');
+
+describe('Modal store', () => {
+ let issue;
+ let issue2;
+ const Store = gl.issueBoards.ModalStore;
+
+ beforeEach(() => {
+ // Setup default state
+ Store.store.issues = [];
+ Store.store.selectedIssues = [];
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [],
+ });
+ issue2 = new ListIssue({
+ title: 'Testing',
+ iid: 2,
+ confidential: false,
+ labels: [],
+ });
+ Store.store.issues.push(issue);
+ Store.store.issues.push(issue2);
+ });
+
+ it('returns selected count', () => {
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles the issue as selected', () => {
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('toggles the issue as un-selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(issue.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all issues as selected', () => {
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('toggles all issues as un-selected', () => {
+ Store.toggleAll();
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(false);
+ expect(issue2.selected).toBe(false);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('toggles all if a single issue is selected', () => {
+ Store.toggleIssue(issue);
+ Store.toggleAll();
+
+ expect(issue.selected).toBe(true);
+ expect(issue2.selected).toBe(true);
+ expect(Store.selectedCount()).toBe(2);
+ });
+
+ it('adds issue to selected array', () => {
+ issue.selected = true;
+ Store.addSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(1);
+ });
+
+ it('removes issue from selected array', () => {
+ Store.addSelectedIssue(issue);
+ Store.removeSelectedIssue(issue);
+
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('returns selected issue index if present', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.selectedIssueIndex(issue)).toBe(0);
+ });
+
+ it('returns -1 if issue is not selected', () => {
+ expect(Store.selectedIssueIndex(issue)).toBe(-1);
+ });
+
+ it('finds the selected issue', () => {
+ Store.toggleIssue(issue);
+
+ expect(Store.findSelectedIssue(issue)).toBe(issue);
+ });
+
+ it('does not find a selected issue', () => {
+ expect(Store.findSelectedIssue(issue)).toBe(undefined);
+ });
+
+ it('does not remove from selected issue if tab is not all', () => {
+ Store.store.activeTab = 'selected';
+
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue);
+
+ expect(Store.store.selectedIssues.length).toBe(1);
+ expect(Store.selectedCount()).toBe(0);
+ });
+
+ it('gets selected issue array with only selected issues', () => {
+ Store.toggleIssue(issue);
+ Store.toggleIssue(issue2);
+ Store.toggleIssue(issue2);
+
+ expect(Store.getSelectedIssues().length).toBe(1);
+ });
+});
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
index ea953d0f5a5..fa9f95e16cd 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6
@@ -1,6 +1,15 @@
-//= require lib/utils/bootstrap_linked_tabs
+require('~/lib/utils/bootstrap_linked_tabs');
(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('Linked Tabs', () => {
preloadFixtures('static/linked_tabs.html.raw');
@@ -10,7 +19,9 @@
describe('when is initialized', () => {
beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function () {});
+ if (!phantomjs) {
+ spyOn(window.history, 'replaceState').and.callFake(function () {});
+ }
});
it('should activate the tab correspondent to the given action', () => {
@@ -36,7 +47,7 @@
describe('on click', () => {
it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(history, 'replaceState').and.callFake(() => {});
+ const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {});
const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line
action: 'show',
@@ -49,10 +60,11 @@
secondTab.click();
- expect(historySpy).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState,
- }, document.title, newState);
+ if (historySpy) {
+ expect(historySpy).toHaveBeenCalledWith({
+ url: newState,
+ }, document.title, newState);
+ }
});
});
});
diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6
index 0c556382980..0bd50588f5a 100644
--- a/spec/javascripts/build_spec.js.es6
+++ b/spec/javascripts/build_spec.js.es6
@@ -1,12 +1,11 @@
/* eslint-disable no-new */
/* global Build */
-/* global Turbolinks */
-//= require lib/utils/datetime_utility
-//= require build
-//= require breakpoints
-//= require jquery.nicescroll
-//= require turbolinks
+require('~/lib/utils/datetime_utility');
+require('~/lib/utils/url_utility');
+require('~/build');
+require('~/breakpoints');
+require('vendor/jquery.nicescroll');
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
@@ -167,7 +166,7 @@ describe('Build', () => {
});
it('reloads the page when the build is done', () => {
- spyOn(Turbolinks, 'visit');
+ spyOn(gl.utils, 'visitUrl');
jasmine.clock().tick(4001);
const [{ success, context }] = $.ajax.calls.argsFor(1);
@@ -177,7 +176,7 @@ describe('Build', () => {
append: true,
});
- expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL);
});
});
});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6
new file mode 100644
index 00000000000..188908d66bd
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/mock_data.js.es6
@@ -0,0 +1,92 @@
+/* eslint-disable no-unused-vars */
+const pipeline = {
+ id: 73,
+ 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',
+ },
+ path: '/root/review-app/pipelines/73',
+ details: {
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73',
+ },
+ duration: null,
+ finished_at: '2017-01-25T00:00:17.130Z',
+ stages: [{
+ name: 'build',
+ title: 'build: failed',
+ status: {
+ icon: 'icon_status_failed',
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ has_details: true,
+ details_path: '/root/review-app/pipelines/73#build',
+ },
+ path: '/root/review-app/pipelines/73#build',
+ dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build',
+ }],
+ artifacts: [],
+ manual_actions: [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1463/play',
+ },
+ {
+ name: 'name',
+ path: '/root/review-app/builds/1490/play',
+ },
+ ],
+ },
+ flags: {
+ latest: true,
+ triggered: false,
+ stuck: false,
+ yaml_errors: false,
+ retryable: true,
+ cancelable: false,
+ },
+ ref:
+ {
+ name: 'master',
+ path: '/root/review-app/tree/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ short_id: 'fbd79f04',
+ title: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ created_at: '2017-01-16T12:13:57.000-05:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ message: 'Update .gitlab-ci.yml',
+ author: {
+ 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',
+ },
+ author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4',
+ },
+ retry_path: '/root/review-app/pipelines/73/retry',
+ created_at: '2017-01-16T17:13:59.800Z',
+ updated_at: '2017-01-25T00:00:17.132Z',
+};
+
+module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
new file mode 100644
index 00000000000..f09c57978a1
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6
@@ -0,0 +1,105 @@
+/* global pipeline, Vue */
+
+require('~/flash');
+require('~/commit/pipelines/pipelines_store');
+require('~/commit/pipelines/pipelines_service');
+require('~/commit/pipelines/pipelines_table');
+require('~/vue_shared/vue_resource_interceptor');
+const pipeline = require('./mock_data');
+
+describe('Pipelines table in Commits and Merge requests', () => {
+ preloadFixtures('static/pipelines_table.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/pipelines_table.html.raw');
+ });
+
+ describe('successfull request', () => {
+ describe('without pipelines', () => {
+ const pipelinesEmptyResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesEmptyResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesEmptyResponse,
+ );
+ });
+
+ it('should render the empty state', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 1);
+ });
+ });
+
+ describe('with pipelines', () => {
+ const pipelinesResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([pipeline]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesResponse,
+ );
+ });
+
+ it('should render a table with the received pipelines', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ done();
+ }, 0);
+ });
+ });
+ });
+
+ describe('unsuccessfull request', () => {
+ const pipelinesErrorResponse = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 500,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(pipelinesErrorResponse);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesErrorResponse,
+ );
+ });
+
+ it('should render empty state', (done) => {
+ const component = new gl.commits.pipelines.PipelinesTableView({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
+
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
new file mode 100644
index 00000000000..789f5dc9f49
--- /dev/null
+++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6
@@ -0,0 +1,33 @@
+require('~/commit/pipelines/pipelines_store');
+
+describe('Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new gl.commits.pipelines.PipelinesStore();
+ });
+
+ // unregister intervals and event handlers
+ afterEach(() => gl.VueRealtimeListener.reset());
+
+ it('should start with a blank state', () => {
+ expect(store.state.pipelines.length).toBe(0);
+ });
+
+ it('should store an array of pipelines', () => {
+ const pipelines = [
+ {
+ id: '1',
+ name: 'pipeline',
+ },
+ {
+ id: '2',
+ name: 'pipeline_2',
+ },
+ ];
+
+ store.storePipelines(pipelines);
+
+ expect(store.state.pipelines.length).toBe(pipelines.length);
+ });
+});
diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6
index bb9a9072f3a..05260760c43 100644
--- a/spec/javascripts/commits_spec.js.es6
+++ b/spec/javascripts/commits_spec.js.es6
@@ -1,10 +1,19 @@
/* global CommitsList */
-//= require jquery.endless-scroll
-//= require pager
-//= require commits
+require('vendor/jquery.endless-scroll');
+require('~/pager');
+require('~/commits');
(() => {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ let phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('Commits List', () => {
beforeEach(() => {
setFixtures(`
@@ -25,7 +34,10 @@
beforeEach(() => {
CommitsList.init(25);
CommitsList.searchField.val('');
- spyOn(history, 'replaceState').and.stub();
+
+ if (!phantomjs) {
+ spyOn(history, 'replaceState').and.stub();
+ }
ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
req.success({
data: '<li>Result</li>',
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index 4d851b2d320..c0bdb89ed63 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,9 +1,7 @@
/* eslint-disable no-new */
-/*= require sidebar */
-/*= require jquery */
-/*= require js.cookie */
-/*= require lib/utils/text_utility */
+require('~/sidebar');
+require('~/lib/utils/text_utility');
((global) => {
describe('Dashboard', () => {
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index 8ece24555c5..d5eec10be42 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/datetime_utility
+require('~/lib/utils/datetime_utility');
(() => {
describe('Date time utils', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index fbfa34a5da7..f956394ef53 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,10 +1,9 @@
/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
/* global CommentsStore */
-//= require vue
-//= require diff_notes/models/discussion
-//= require diff_notes/models/note
-//= require diff_notes/stores/comments
+require('~/diff_notes/models/discussion');
+require('~/diff_notes/models/note');
+require('~/diff_notes/stores/comments');
(() => {
function createDiscussion(noteId = 1, resolved = true) {
diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6
index 056e4d41e93..b1838045a06 100644
--- a/spec/javascripts/environments/environment_actions_spec.js.es6
+++ b/spec/javascripts/environments/environment_actions_spec.js.es6
@@ -1,5 +1,4 @@
-//= require vue
-//= require environments/components/environment_actions
+require('~/environments/components/environment_actions');
describe('Actions Component', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6
index 950a5d53fad..a6a587e69f5 100644
--- a/spec/javascripts/environments/environment_external_url_spec.js.es6
+++ b/spec/javascripts/environments/environment_external_url_spec.js.es6
@@ -1,5 +1,4 @@
-//= require vue
-//= require environments/components/environment_external_url
+require('~/environments/components/environment_external_url');
describe('External URL Component', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6
index c178b9cc1ec..e6fb0d00290 100644
--- a/spec/javascripts/environments/environment_item_spec.js.es6
+++ b/spec/javascripts/environments/environment_item_spec.js.es6
@@ -1,6 +1,5 @@
-//= require vue
-//= require timeago
-//= require environments/components/environment_item
+window.timeago = require('timeago.js');
+require('~/environments/components/environment_item');
describe('Environment item', () => {
preloadFixtures('static/environments/table.html.raw');
@@ -120,7 +119,7 @@ describe('Environment item', () => {
},
],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 95796f23894..043b8708a6e 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -1,5 +1,5 @@
-//= require vue
-//= require environments/components/environment_rollback
+require('~/environments/components/environment_rollback');
+
describe('Rollback Component', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
index 20e11ca3738..87eda136122 100644
--- a/spec/javascripts/environments/environment_spec.js.es6
+++ b/spec/javascripts/environments/environment_spec.js.es6
@@ -1,19 +1,17 @@
/* global Vue, environment */
-//= require vue
-//= require vue-resource
-//= require flash
-//= require environments/stores/environments_store
-//= require environments/components/environment
-//= require ./mock_data
+require('~/flash');
+require('~/environments/stores/environments_store');
+require('~/environments/components/environment');
+require('./mock_data');
describe('Environment', () => {
- preloadFixtures('environments/environments');
+ preloadFixtures('static/environments/environments.html.raw');
let component;
beforeEach(() => {
- loadFixtures('environments/environments');
+ loadFixtures('static/environments/environments.html.raw');
});
describe('successfull request', () => {
diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6
index bb998a32f32..2dfce5ba824 100644
--- a/spec/javascripts/environments/environment_stop_spec.js.es6
+++ b/spec/javascripts/environments/environment_stop_spec.js.es6
@@ -1,5 +1,5 @@
-//= require vue
-//= require environments/components/environment_stop
+require('~/environments/components/environment_stop');
+
describe('Stop Component', () => {
preloadFixtures('static/environments/element.html.raw');
diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6
index 17c00acf63e..9a8300d3832 100644
--- a/spec/javascripts/environments/environments_store_spec.js.es6
+++ b/spec/javascripts/environments/environments_store_spec.js.es6
@@ -1,8 +1,7 @@
/* global environmentsList */
-//= require vue
-//= require environments/stores/environments_store
-//= require ./mock_data
+require('~/environments/stores/environments_store');
+require('./mock_data');
(() => {
describe('Store', () => {
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 8ecd01f9a83..80e1cbc6f4d 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable no-unused-vars */
+
const environmentsList = [
{
id: 31,
@@ -50,7 +50,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -105,7 +105,7 @@ const environmentsList = [
},
manual_actions: [],
},
- 'stoppable?': false,
+ 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -116,7 +116,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
@@ -127,13 +127,15 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
- 'stoppable?': true,
+ 'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
];
+window.environmentsList = environmentsList;
+
const environment = {
id: 4,
name: 'production',
@@ -141,9 +143,11 @@ const environment = {
external_url: 'http://production.',
environment_type: null,
last_deployment: {},
- 'stoppable?': false,
+ 'stop_action?': false,
environment_path: '/root/review-app/environments/4',
stop_path: '/root/review-app/environments/4/stop',
created_at: '2016-12-16T11:51:04.690Z',
updated_at: '2016-12-16T12:04:51.133Z',
};
+
+window.environment = environment;
diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6
index 3949c5615d5..ba5eb81defc 100644
--- a/spec/javascripts/extensions/array_spec.js.es6
+++ b/spec/javascripts/extensions/array_spec.js.es6
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require extensions/array */
+require('~/extensions/array');
(function() {
describe('Array extensions', function() {
diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6
index c5b86d35204..2d8a128ed33 100644
--- a/spec/javascripts/extensions/element_spec.js.es6
+++ b/spec/javascripts/extensions/element_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require extensions/element */
+require('~/extensions/element');
(() => {
describe('Element extensions', function () {
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index 5cd0e5ab0f0..c0bb0419814 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require extensions/jquery */
+require('~/extensions/jquery');
(function() {
describe('jQuery extensions', function() {
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
index 3b71c255b30..2467ed78459 100644
--- a/spec/javascripts/extensions/object_spec.js.es6
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require extensions/object */
+require('~/extensions/object');
describe('Object extensions', () => {
describe('assign', () => {
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
new file mode 100644
index 00000000000..fa9d03c8a9a
--- /dev/null
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6
@@ -0,0 +1,75 @@
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown');
+require('~/filtered_search/dropdown_user');
+
+(() => {
+ describe('Dropdown User', () => {
+ describe('getSearchInput', () => {
+ let dropdownUser;
+
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
+
+ dropdownUser = new gl.DropdownUser();
+ });
+
+ it('should not return the double quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '"johnny appleseed',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
+ });
+
+ it('should not return the single quote found in value', () => {
+ spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
+ lastToken: {
+ value: '\'larry boy',
+ },
+ });
+
+ expect(dropdownUser.getSearchInput()).toBe('larry boy');
+ });
+ });
+
+ describe('config droplabAjaxFilter\'s endpoint', () => {
+ beforeEach(() => {
+ spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ });
+
+ it('should return endpoint', () => {
+ window.gon = {
+ relative_url_root: '',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint when relative_url_root is undefined', () => {
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json');
+ });
+
+ it('should return endpoint with relative url when available', () => {
+ window.gon = {
+ relative_url_root: '/gitlab_directory',
+ };
+ const dropdown = new gl.DropdownUser();
+
+ expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json');
+ });
+
+ afterEach(() => {
+ window.gon = {};
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
index 89e49b7c511..1e2d7582d5b 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6
@@ -1,7 +1,7 @@
-//= require extensions/array
-//= require filtered_search/dropdown_utils
-//= require filtered_search/filtered_search_tokenizer
-//= require filtered_search/filtered_search_dropdown_manager
+require('~/extensions/array');
+require('~/filtered_search/dropdown_utils');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
(() => {
describe('Dropdown Utils', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
index 4bd45eb457d..ed0b0196ec4 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6
@@ -1,6 +1,6 @@
-//= require extensions/array
-//= require filtered_search/filtered_search_tokenizer
-//= require filtered_search/filtered_search_dropdown_manager
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
(() => {
describe('Filtered Search Dropdown Manager', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
index c8b5c2b36ad..98959dda242 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6
@@ -1,11 +1,9 @@
-/* global Turbolinks */
-
-//= require turbolinks
-//= require lib/utils/common_utils
-//= require filtered_search/filtered_search_token_keys
-//= require filtered_search/filtered_search_tokenizer
-//= require filtered_search/filtered_search_dropdown_manager
-//= require filtered_search/filtered_search_manager
+require('~/lib/utils/url_utility');
+require('~/lib/utils/common_utils');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
+require('~/filtered_search/filtered_search_dropdown_manager');
+require('~/filtered_search/filtered_search_manager');
(() => {
describe('Filtered Search Manager', () => {
@@ -23,6 +21,7 @@
`);
spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
@@ -37,7 +36,7 @@
it('should search with a single word', () => {
getInput().value = 'searchTerm';
- spyOn(Turbolinks, 'visit').and.callFake((url) => {
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
});
@@ -47,7 +46,7 @@
it('should search with multiple words', () => {
getInput().value = 'awesome search terms';
- spyOn(Turbolinks, 'visit').and.callFake((url) => {
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
});
@@ -57,7 +56,7 @@
it('should search with special characters', () => {
getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(Turbolinks, 'visit').and.callFake((url) => {
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
});
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
index 9d9097419ea..cf409a7e509 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6
@@ -1,5 +1,5 @@
-//= require extensions/array
-//= require filtered_search/filtered_search_token_keys
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
(() => {
describe('Filtered Search Token Keys', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
index ac7f8e9cbcd..84c0e9cbfe2 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6
@@ -1,6 +1,6 @@
-//= require extensions/array
-//= require filtered_search/filtered_search_token_keys
-//= require filtered_search/filtered_search_tokenizer
+require('~/extensions/array');
+require('~/filtered_search/filtered_search_token_keys');
+require('~/filtered_search/filtered_search_tokenizer');
(() => {
describe('Filtered Search Tokenizer', () => {
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
new file mode 100644
index 00000000000..0e7c2351b66
--- /dev/null
+++ b/spec/javascripts/fixtures/branches.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('branches/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'branches/new_branch.html.raw' do |example|
+ get :new,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
index 1ea1725c561..59edc0396d2 100644
--- a/spec/javascripts/fixtures/environments/table.html.haml
+++ b/spec/javascripts/fixtures/environments/table.html.haml
@@ -3,7 +3,7 @@
%tr
%th Environment
%th Last deployment
- %th Build
+ %th Job
%th Commit
%th
%th
diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml
deleted file mode 100644
index 4db2ef604de..00000000000
--- a/spec/javascripts/fixtures/header.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class
- .container-fluid
- .header-content
- %button.side-nav-toggle
- %span.sr-only
- Toggle navigation
- %i.fa.fa-bars
- %button.navbar-toggle
- %span.sr-only
- Toggle navigation
- %i.fa.fa-ellipsis-v
- .navbar-collapse.collapse
- %ui.nav.navbar-nav
- %li.hidden-sm.hidden-xs
- %li.visible-sm.visible-xs
- %li
- %a
- %i.fa.fa-bell.fa-fw
- %span.badge.todos-pending-count
- %li
- %a
- %i.fa.fa-plus.fa-fw
- %li.header-user.dropdown
- %a
- %img
- %span.caret
- .dropdown-menu-nav
- .dropdown-menu-align-right
- %ul
- %li
- %a.profile-link
- %li
- %a
- %li.divider
- %li.sign-out-link
diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml
deleted file mode 100644
index f06629e5ecc..00000000000
--- a/spec/javascripts/fixtures/new_branch.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%form.js-create-branch-form
- %input.js-branch-name
- .js-branch-name-error
- %input{id: "ref"}
diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml
new file mode 100644
index 00000000000..fbe4a434f76
--- /dev/null
+++ b/spec/javascripts/fixtures/pipelines_table.html.haml
@@ -0,0 +1,2 @@
+#commit-pipeline-table-view{ data: { endpoint: "endpoint" } }
+.pipeline-svgs{ data: { "commit_icon_svg": "svg"} }
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
new file mode 100644
index 00000000000..56513219e1e
--- /dev/null
+++ b/spec/javascripts/fixtures/projects.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('projects/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'projects/dashboard.html.raw' do |example|
+ get :show,
+ namespace_id: project.namespace.to_param,
+ id: project.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6
index 99cebb32a8b..c61c32f8a13 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js.es6
+++ b/spec/javascripts/gfm_auto_complete_spec.js.es6
@@ -1,6 +1,6 @@
-//= require gfm_auto_complete
-//= require jquery
-//= require jquery.atwho
+require('~/gfm_auto_complete');
+require('vendor/jquery.caret');
+require('vendor/jquery.atwho');
const global = window.gl || (window.gl = {});
const GfmAutoComplete = global.GfmAutoComplete;
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index 06fa64b1b4e..317f38c5888 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,11 +1,9 @@
/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */
-/* global Turbolinks */
-/*= require jquery */
-/*= require gl_dropdown */
-/*= require turbolinks */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
+require('~/gl_dropdown');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('~/lib/utils/url_utility');
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
@@ -44,6 +42,7 @@
describe('Dropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
@@ -112,13 +111,13 @@
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(Turbolinks, 'visit').and.stub();
+ spyOn(gl.utils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index f68fd9e00d7..733023481f5 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,7 +1,6 @@
/* eslint-disable space-before-function-paren, arrow-body-style */
-//= require jquery
-//= require gl_field_errors
+require('~/gl_field_errors');
((global) => {
preloadFixtures('static/gl_field_errors.html.raw');
diff --git a/spec/javascripts/gl_form_spec.js.es6 b/spec/javascripts/gl_form_spec.js.es6
new file mode 100644
index 00000000000..71d6e2a7e22
--- /dev/null
+++ b/spec/javascripts/gl_form_spec.js.es6
@@ -0,0 +1,123 @@
+/* global autosize */
+
+window.autosize = require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
+require('~/lib/utils/common_utils');
+
+describe('GLForm', () => {
+ const global = window.gl || (window.gl = {});
+ const GLForm = global.GLForm;
+
+ it('should be defined in the global scope', () => {
+ expect(GLForm).toBeDefined();
+ });
+
+ describe('when instantiated', function () {
+ beforeEach((done) => {
+ this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ this.textarea = this.form.find('textarea');
+ spyOn($.prototype, 'off').and.returnValue(this.textarea);
+ spyOn($.prototype, 'on').and.returnValue(this.textarea);
+ spyOn($.prototype, 'css');
+ spyOn(window, 'autosize');
+
+ this.glForm = new GLForm(this.form);
+ setTimeout(() => {
+ $.prototype.off.calls.reset();
+ $.prototype.on.calls.reset();
+ $.prototype.css.calls.reset();
+ autosize.calls.reset();
+ done();
+ });
+ });
+
+ describe('.setupAutosize', () => {
+ beforeEach((done) => {
+ this.glForm.setupAutosize();
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ it('should register an autosize event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('autosize:resized');
+ expect($.prototype.on).toHaveBeenCalledWith('autosize:resized', jasmine.any(Function));
+ });
+
+ it('should register a mouseup event handler on the textarea', () => {
+ expect($.prototype.off).toHaveBeenCalledWith('mouseup.autosize');
+ expect($.prototype.on).toHaveBeenCalledWith('mouseup.autosize', jasmine.any(Function));
+ });
+
+ it('should autosize the textarea', () => {
+ expect(autosize).toHaveBeenCalledWith(jasmine.any(Object));
+ });
+
+ it('should set the resize css property to vertical', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('resize', 'vertical');
+ });
+ });
+
+ describe('.setHeightData', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ this.glForm.setHeightData();
+ });
+
+ it('should set the height data attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+ });
+
+ describe('.destroyAutosize', () => {
+ describe('when called', () => {
+ beforeEach(() => {
+ spyOn($.prototype, 'data');
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn(window, 'outerHeight').and.returnValue(400);
+ spyOn(autosize, 'destroy');
+
+ this.glForm.destroyAutosize();
+ });
+
+ it('should call outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalled();
+ });
+
+ it('should get data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height');
+ });
+
+ it('should call autosize destroy', () => {
+ expect(autosize.destroy).toHaveBeenCalledWith(this.textarea);
+ });
+
+ it('should set the data-height attribute', () => {
+ expect($.prototype.data).toHaveBeenCalledWith('height', 200);
+ });
+
+ it('should set the outerHeight', () => {
+ expect($.prototype.outerHeight).toHaveBeenCalledWith(200);
+ });
+
+ it('should set the css', () => {
+ expect($.prototype.css).toHaveBeenCalledWith('max-height', window.outerHeight);
+ });
+ });
+
+ it('should return undefined if the data-height equals the outerHeight', () => {
+ spyOn($.prototype, 'outerHeight').and.returnValue(200);
+ spyOn($.prototype, 'data').and.returnValue(200);
+ spyOn(autosize, 'destroy');
+ expect(this.glForm.destroyAutosize()).toBeUndefined();
+ expect(autosize.destroy).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index d76fcc5206a..a954bb60560 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -3,7 +3,7 @@
/* global ContributorsGraph */
/* global ContributorsMasterGraph */
-//= require graphs/stat_graph_contributors_graph
+require('~/graphs/stat_graph_contributors_graph');
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 63f28dfb8ad..b15764abe8c 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */
/* global ContributorsStatGraphUtil */
-//= require graphs/stat_graph_contributors_util
+require('~/graphs/stat_graph_contributors_util');
describe("ContributorsStatGraphUtil", function () {
describe("#parse_log", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 71b589e6b83..876c23361bc 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable quotes */
/* global StatGraph */
-//= require graphs/stat_graph
+require('~/graphs/stat_graph');
describe("StatGraph", function () {
describe("#get_log", function () {
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index b846c5ab00b..2b263b71b7d 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -1,12 +1,12 @@
/* eslint-disable space-before-function-paren, no-var */
-/*= require header */
-/*= require lib/utils/text_utility */
-/*= require jquery */
+
+require('~/header');
+require('~/lib/utils/text_utility');
(function() {
describe('Header', function() {
var todosPendingCount = '.todos-pending-count';
- var fixtureTemplate = 'static/header.html.raw';
+ var fixtureTemplate = 'issues/open-issue.html.raw';
function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden');
diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6
index 92a20687ec5..d3c37d39431 100644
--- a/spec/javascripts/helpers/class_spec_helper.js.es6
+++ b/spec/javascripts/helpers/class_spec_helper.js.es6
@@ -1,5 +1,3 @@
-/* eslint-disable no-unused-vars */
-
class ClassSpecHelper {
static itShouldBeAStaticMethod(base, method) {
return it('should be a static method', () => {
@@ -7,3 +5,5 @@ class ClassSpecHelper {
});
}
}
+
+window.ClassSpecHelper = ClassSpecHelper;
diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js.es6 b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
index d1155f1bd1e..0a61e561640 100644
--- a/spec/javascripts/helpers/class_spec_helper_spec.js.es6
+++ b/spec/javascripts/helpers/class_spec_helper_spec.js.es6
@@ -1,5 +1,6 @@
/* global ClassSpecHelper */
-//= require ./class_spec_helper
+
+require('./class_spec_helper');
describe('ClassSpecHelper', () => {
describe('.itShouldBeAStaticMethod', function () {
diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6
index 917a6267b92..26d87cc5931 100644
--- a/spec/javascripts/issuable_spec.js.es6
+++ b/spec/javascripts/issuable_spec.js.es6
@@ -1,8 +1,7 @@
/* global Issuable */
-/* global Turbolinks */
-//= require issuable
-//= require turbolinks
+require('~/lib/utils/url_utility');
+require('~/issuable');
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
@@ -42,39 +41,39 @@
});
it('should contain only the default parameters', () => {
- spyOn(Turbolinks, 'visit');
+ spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm);
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
it('should filter for the phrase "broken"', () => {
- spyOn(Turbolinks, 'visit');
+ spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
it('should keep query parameters after modifying filter', () => {
- spyOn(Turbolinks, 'visit');
+ spyOn(gl.utils, 'visitUrl');
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
- expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});
});
});
diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6
index a1e979e8d09..cb068a4f879 100644
--- a/spec/javascripts/issuable_time_tracker_spec.js.es6
+++ b/spec/javascripts/issuable_time_tracker_spec.js.es6
@@ -1,10 +1,11 @@
/* eslint-disable */
-//= require jquery
-//= require vue
-//= require issuable/time_tracking/components/time_tracker
+
+require('jquery');
+require('vue');
+require('~/issuable/time_tracking/components/time_tracker');
function initTimeTrackingComponent(opts) {
- fixture.set(`
+ setFixtures(`
<div>
<div id="mock-container"></div>
</div>
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 673a4b3c07a..5b0b7aa7903 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
/* global Issue */
-/*= require lib/utils/text_utility */
-/*= require issue */
+require('~/lib/utils/text_utility');
+require('~/issue');
(function() {
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 0d19b4a25b9..37e038c16da 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -2,17 +2,15 @@
/* global IssuableContext */
/* global LabelsSelect */
-//= require lib/utils/type_utility
-//= require jquery
-//= require bootstrap
-//= require gl_dropdown
-//= require select2
-//= require jquery.nicescroll
-//= require api
-//= require create_label
-//= require issuable_context
-//= require users_select
-//= require labels_select
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('select2');
+require('vendor/jquery.nicescroll');
+require('~/api');
+require('~/create_label');
+require('~/issuable_context');
+require('~/users_select');
+require('~/labels_select');
(() => {
let saveLabelCount = 0;
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index 1ce8f28e568..006ede21093 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/common_utils
+require('~/lib/utils/common_utils');
(() => {
describe('common_utils', () => {
@@ -10,9 +10,9 @@
// IE11 will return a relative pathname while other browsers will return a full pathname.
// parseUrl uses an anchor element for parsing an url. With relative urls, the anchor
// element will create an absolute url relative to the current execution context.
- // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute
- // url starting with '/teaspoon'.
- expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22');
+ // The JavaScript test suite is executed at '/' which will lead to an absolute url
+ // starting with '/'.
+ expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22');
});
});
@@ -41,10 +41,48 @@
});
});
+ describe('gl.utils.handleLocationHash', () => {
+ beforeEach(() => {
+ spyOn(window.document, 'getElementById').and.callThrough();
+ });
+
+ function expectGetElementIdToHaveBeenCalledWith(elementId) {
+ expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
+ }
+
+ it('decodes hash parameter', () => {
+ window.history.pushState({}, null, '#random-hash');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('random-hash');
+ expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
+ });
+
+ it('decodes cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#definição');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+
+ it('decodes encoded cyrillic hash parameter', () => {
+ window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
+ gl.utils.handleLocationHash();
+
+ expectGetElementIdToHaveBeenCalledWith('definição');
+ expectGetElementIdToHaveBeenCalledWith('user-content-definição');
+ });
+ });
+
describe('gl.utils.getParameterByName', () => {
+ beforeEach(() => {
+ window.history.pushState({}, null, '?scope=all&p=2');
+ });
+
it('should return valid parameter', () => {
- const value = gl.utils.getParameterByName('reporter');
- expect(value).toBe('Console');
+ const value = gl.utils.getParameterByName('scope');
+ expect(value).toBe('all');
});
it('should return invalid parameter', () => {
@@ -69,5 +107,37 @@
expect(normalized[NGINX].nginx).toBe('ok');
});
});
+
+ describe('gl.utils.isMetaClick', () => {
+ it('should identify meta click on Windows/Linux', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify meta click on macOS', () => {
+ const e = {
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+
+ it('should identify as meta click on middle-click or Mouse-wheel click', () => {
+ const e = {
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ };
+
+ expect(gl.utils.isMetaClick(e)).toBe(true);
+ });
+ });
});
})();
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6
index e97356b65d5..86ade66ec29 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js.es6
+++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/text_utility
+require('~/lib/utils/text_utility');
(() => {
describe('text_utility', () => {
@@ -21,5 +21,19 @@
expect(largeFont > regular).toBe(true);
});
});
+
+ describe('gl.text.pluralize', () => {
+ it('returns pluralized', () => {
+ expect(gl.text.pluralize('test', 2)).toBe('tests');
+ });
+
+ it('returns pluralized when count is 0', () => {
+ expect(gl.text.pluralize('test', 0)).toBe('tests');
+ });
+
+ it('does not return pluralized', () => {
+ expect(gl.text.pluralize('test', 1)).toBe('test');
+ });
+ });
});
})();
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 6605986c33a..8b196f7720f 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */
/* global LineHighlighter */
-/*= require line_highlighter */
+require('~/line_highlighter');
(function() {
describe('LineHighlighter', function() {
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index f644d39b1c7..25cfa9e9479 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable space-before-function-paren, no-return-assign */
/* global MergeRequest */
-/*= require merge_request */
+require('~/merge_request');
(function() {
describe('MergeRequest', function() {
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 98201fb98ed..92a0f1c05f7 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,11 +1,20 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-/*= require merge_request_tabs */
-//= require breakpoints
-//= require lib/utils/common_utils
-//= require jquery.scrollTo
+require('~/merge_request_tabs');
+require('~/breakpoints');
+require('~/lib/utils/common_utils');
+require('vendor/jquery.scrollTo');
(function () {
+ // TODO: remove this hack!
+ // PhantomJS causes spyOn to panic because replaceState isn't "writable"
+ var phantomjs;
+ try {
+ phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable;
+ } catch (err) {
+ phantomjs = false;
+ }
+
describe('MergeRequestTabs', function () {
var stubLocation = {};
var setLocation = function (stubs) {
@@ -22,9 +31,11 @@
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
setLocation();
- this.spies = {
- history: spyOn(window.history, 'replaceState').and.callFake(function () {})
- };
+ if (!phantomjs) {
+ this.spies = {
+ history: spyOn(window.history, 'replaceState').and.callFake(function () {})
+ };
+ }
});
describe('#activateTab', function () {
@@ -50,6 +61,56 @@
expect($('#diffs')).toHaveClass('active');
});
});
+ describe('#opensInNewTab', function () {
+ var commitsLink;
+ var tabUrl;
+
+ beforeEach(function () {
+ commitsLink = '.commits-tab li a';
+ tabUrl = $(commitsLink).attr('href');
+
+ spyOn($.fn, 'attr').and.returnValue(tabUrl);
+ });
+ it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: true,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: true,
+ ctrlKey: false,
+ which: 1,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
+ spyOn(window, 'open').and.callFake(function (url, name) {
+ expect(url).toEqual(tabUrl);
+ expect(name).toEqual('_blank');
+ });
+
+ this.class.clickTab({
+ metaKey: false,
+ ctrlKey: false,
+ which: 2,
+ stopImmediatePropagation: function () {}
+ });
+ });
+ });
describe('#setCurrentAction', function () {
beforeEach(function () {
@@ -98,10 +159,11 @@
pathname: '/foo/bar/merge_requests/1'
});
newState = this.subject('commits');
- expect(this.spies.history).toHaveBeenCalledWith({
- turbolinks: true,
- url: newState
- }, document.title, newState);
+ if (!phantomjs) {
+ expect(this.spies.history).toHaveBeenCalledWith({
+ url: newState
+ }, document.title, newState);
+ }
});
it('treats "show" like "notes"', function () {
setLocation({
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index bf45100af03..8cefdd2409d 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,7 +1,8 @@
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
-/*= require merge_request_widget */
-/*= require lib/utils/datetime_utility */
+require('~/merge_request_widget');
+require('~/smart_interval');
+require('~/lib/utils/datetime_utility');
(function() {
describe('MergeRequestWidget', function() {
@@ -21,7 +22,11 @@
normal: "Build {{status}}"
},
gitlab_icon: "gitlab_logo.png",
- builds_path: "http://sampledomain.local/sampleBuildsPath"
+ ci_pipeline: 80,
+ ci_sha: "12a34bc5",
+ builds_path: "http://sampledomain.local/sampleBuildsPath",
+ commits_path: "http://sampledomain.local/commits",
+ pipeline_path: "http://sampledomain.local/pipelines"
};
this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
@@ -118,10 +123,11 @@
});
});
- return describe('getCIStatus', function() {
+ describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
"title": "Sample MR title",
+ "pipeline": 80,
"sha": "12a34bc5",
"status": "success",
"coverage": 98
@@ -165,6 +171,22 @@
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
+ it('should update the pipeline URL when the pipeline changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.pipeline += 1;
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
+ it('should update the commit URL when the sha changes', function() {
+ var spy;
+ spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
+ this["class"].getCIStatus(false);
+ this.ciStatusData.sha = "9b50b99a";
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalled();
+ });
});
});
}).call(this);
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
index a1c2fe3df37..7cdade01e00 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6
@@ -1,7 +1,7 @@
/* eslint-disable no-new */
-//= require flash
-//= require mini_pipeline_graph_dropdown
+require('~/flash');
+require('~/mini_pipeline_graph_dropdown');
(() => {
describe('Mini Pipeline Graph Dropdown', () => {
@@ -31,7 +31,7 @@
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
@@ -41,7 +41,7 @@
it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
- new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
+ new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 8259d553f1b..1d014502c2a 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,14 +1,14 @@
/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */
/* global NewBranchForm */
-/*= require jquery-ui/autocomplete */
-/*= require new_branch_form */
+require('jquery-ui/ui/autocomplete');
+require('~/new_branch_form');
(function() {
describe('Branch', function() {
return describe('create a new branch', function() {
var expectToHaveError, fillNameWith;
- preloadFixtures('static/new_branch.html.raw');
+ preloadFixtures('branches/new_branch.html.raw');
fillNameWith = function(value) {
return $('.js-branch-name').val(value).trigger('blur');
};
@@ -16,7 +16,7 @@
return expect($('.js-branch-name-error span').text()).toEqual(error);
};
beforeEach(function() {
- loadFixtures('static/new_branch.html.raw');
+ loadFixtures('branches/new_branch.html.raw');
$('form').on('submit', function(e) {
return e.preventDefault();
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 015c35dfca7..af495787c54 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,10 +1,10 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
-/*= require notes */
-/*= require autosize */
-/*= require gl_form */
-/*= require lib/utils/text_utility */
+require('~/notes');
+require('vendor/autosize');
+require('~/gl_form');
+require('~/lib/utils/text_utility');
(function() {
window.gon || (window.gon = {});
diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6
index f0f9ad7430d..72770a702d3 100644
--- a/spec/javascripts/pipelines_spec.js.es6
+++ b/spec/javascripts/pipelines_spec.js.es6
@@ -1,4 +1,9 @@
-//= require pipelines
+require('~/pipelines');
+
+// Fix for phantomJS
+if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) {
+ Element.prototype.matches = Element.prototype.webkitMatchesSelector;
+}
(() => {
describe('Pipelines', () => {
diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6
index 7a04fba5f7f..a4662cfb557 100644
--- a/spec/javascripts/pretty_time_spec.js.es6
+++ b/spec/javascripts/pretty_time_spec.js.es6
@@ -1,4 +1,4 @@
-//= require lib/utils/pretty_time
+require('~/lib/utils/pretty_time');
(() => {
const prettyTime = gl.utils.prettyTime;
diff --git a/spec/javascripts/project_dashboard_spec.js.es6 b/spec/javascripts/project_dashboard_spec.js.es6
new file mode 100644
index 00000000000..24833b4eb57
--- /dev/null
+++ b/spec/javascripts/project_dashboard_spec.js.es6
@@ -0,0 +1,86 @@
+require('~/sidebar');
+
+(() => {
+ describe('Project dashboard page', () => {
+ let $pageWithSidebar = null;
+ let $sidebarToggle = null;
+ let sidebar = null;
+ const fixtureTemplate = 'projects/dashboard.html.raw';
+
+ const assertSidebarStateExpanded = (shouldBeExpanded) => {
+ expect(sidebar.isExpanded).toBe(shouldBeExpanded);
+ expect($pageWithSidebar.hasClass('page-sidebar-expanded')).toBe(shouldBeExpanded);
+ };
+
+ preloadFixtures(fixtureTemplate);
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+
+ $pageWithSidebar = $('.page-with-sidebar');
+ $sidebarToggle = $('.toggle-nav-collapse');
+
+ // otherwise instantiating the Sidebar for the second time
+ // won't do anything, as the Sidebar is a singleton class
+ gl.Sidebar.singleton = null;
+ sidebar = new gl.Sidebar();
+ });
+
+ it('can show the sidebar when the toggler is clicked', () => {
+ assertSidebarStateExpanded(false);
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+ });
+
+ it('should dismiss the sidebar when clone button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const cloneButton = $('.project-clone-holder a.clone-dropdown-btn');
+ cloneButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when download button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const downloadButton = $('.project-action-button .btn:has(i.fa-download)');
+ downloadButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when add button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const addButton = $('.project-action-button .btn:has(i.fa-plus)');
+ addButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when notification button clicked', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ const notifButton = $('.js-notification-toggle-btns .notifications-btn');
+ notifButton.click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the body', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('body').click();
+ assertSidebarStateExpanded(false);
+ });
+
+ it('should dismiss the sidebar when clicking on the project description header', () => {
+ $sidebarToggle.click();
+ assertSidebarStateExpanded(true);
+
+ $('.project-home-panel').click();
+ assertSidebarStateExpanded(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 0202c9ba85e..bfe3d2df79d 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,27 +1,28 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */
-
/* global Project */
-/*= require bootstrap */
-/*= require select2 */
-/*= require lib/utils/type_utility */
-/*= require gl_dropdown */
-/*= require api */
-/*= require project_select */
-/*= require project */
+require('select2/select2.js');
+require('~/lib/utils/type_utility');
+require('~/gl_dropdown');
+require('~/api');
+require('~/project_select');
+require('~/project');
(function() {
- window.gon || (window.gon = {});
-
- window.gon.api_version = 'v3';
-
describe('Project Title', function() {
preloadFixtures('static/project_title.html.raw');
+ loadJSONFixtures('projects.json');
+
beforeEach(function() {
loadFixtures('static/project_title.html.raw');
+
+ window.gon = {};
+ window.gon.api_version = 'v3';
+
return this.project = new Project();
});
- return describe('project list', function() {
+
+ describe('project list', function() {
var fakeAjaxResponse = function fakeAjaxResponse(req) {
var d;
expect(req.url).toBe('/api/v3/projects.json?simple=true');
@@ -48,5 +49,9 @@
return expect($('.header-content').hasClass('open')).toBe(false);
});
});
+
+ afterEach(() => {
+ window.gon = {};
+ });
});
}).call(this);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 942778229b5..f7636865aa1 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,11 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
/* global Sidebar */
-/*= require right_sidebar */
-/*= require jquery */
-/*= require js.cookie */
-
-/*= require extensions/jquery.js */
+require('~/right_sidebar');
+require('~/extensions/jquery.js');
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -37,6 +34,8 @@
describe('RightSidebar', function() {
var fixtureName = 'issues/open-issue.html.raw';
preloadFixtures(fixtureName);
+ loadJSONFixtures('todos.json');
+
beforeEach(function() {
loadFixtures(fixtureName);
this.sidebar = new Sidebar;
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 7ac9710654f..9572b52ec1e 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,13 +1,10 @@
/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
-/*= require gl_dropdown */
-/*= require search_autocomplete */
-/*= require jquery */
-/*= require lib/utils/common_utils */
-/*= require lib/utils/type_utility */
-/*= require fuzzaldrin-plus */
-/*= require turbolinks */
-/*= require jquery.turbolinks */
+require('~/gl_dropdown');
+require('~/search_autocomplete');
+require('~/lib/utils/common_utils');
+require('~/lib/utils/type_utility');
+require('vendor/fuzzaldrin-plus');
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -17,11 +14,6 @@
userId = 1;
- window.gon || (window.gon = {});
-
- window.gon.current_user_id = userId;
- window.gon.current_username = userName;
-
dashboardIssuesPath = '/dashboard/issues';
dashboardMRsPath = '/dashboard/merge_requests';
@@ -117,13 +109,25 @@
preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
loadFixtures('static/search_autocomplete.html.raw');
+ widget = new gl.SearchAutocomplete;
+ // Prevent turbolinks from triggering within gl_dropdown
+ spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
+
+ window.gon = {};
+ window.gon.current_user_id = userId;
+ window.gon.current_username = userName;
+
return widget = new gl.SearchAutocomplete;
});
+
+ afterEach(function() {
+ window.gon = {};
+ });
it('should show Dashboard specific dropdown menu', function() {
var list;
addBodyAttributes();
mockDashboardOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
});
@@ -131,7 +135,7 @@
var list;
addBodyAttributes('group');
mockGroupOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, groupIssuesPath, groupMRsPath);
});
@@ -139,7 +143,7 @@
var list;
addBodyAttributes('project');
mockProjectOptions();
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
@@ -148,7 +152,7 @@
addBodyAttributes('project');
mockProjectOptions();
widget.searchInput.val('help');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
list = widget.wrap.find('.dropdown-menu').find('ul');
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
@@ -159,7 +163,7 @@
addBodyAttributes();
mockDashboardOptions(true);
var submitSpy = spyOnEvent('form', 'submit');
- widget.searchInput.focus();
+ widget.searchInput.triggerHandler('focus');
widget.wrap.trigger($.Event('keydown', { which: DOWN }));
var enterKeyEvent = $.Event('keydown', { which: ENTER });
widget.searchInput.trigger(enterKeyEvent);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 386fc8f514e..602ac01aec3 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,8 +1,8 @@
/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
-/*= require copy_as_gfm */
-/*= require shortcuts_issuable */
+require('~/copy_as_gfm');
+require('~/shortcuts_issuable');
(function() {
describe('ShortcutsIssuable', function() {
@@ -11,9 +11,9 @@
beforeEach(function() {
loadFixtures(fixtureName);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
- return this.shortcut = new ShortcutsIssuable();
+ this.shortcut = new ShortcutsIssuable();
});
- return describe('#replyWithSelectedText', function() {
+ describe('#replyWithSelectedText', function() {
var stubSelection;
// Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
stubSelection = function(html) {
@@ -24,56 +24,57 @@
};
};
beforeEach(function() {
- return this.selector = 'form.js-main-target-form textarea#note_note';
+ this.selector = 'form.js-main-target-form textarea#note_note';
});
describe('with empty selection', function() {
- return it('does nothing', function() {
- stubSelection('');
+ it('does not return an error', function() {
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe('');
+ expect($(this.selector).val()).toBe('');
+ });
+ it('triggers `input`', function() {
+ var focused = false;
+ $(this.selector).on('focus', function() {
+ focused = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ expect(focused).toBe(true);
});
});
describe('with any selection', function() {
beforeEach(function() {
- return stubSelection('<p>Selected text.</p>');
+ stubSelection('<p>Selected text.</p>');
});
it('leaves existing input intact', function() {
$(this.selector).val('This text was already here.');
expect($(this.selector).val()).toBe('This text was already here.');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
+ expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
});
it('triggers `input`', function() {
- var triggered;
- triggered = false;
+ var triggered = false;
$(this.selector).on('input', function() {
- return triggered = true;
+ triggered = true;
});
this.shortcut.replyWithSelectedText();
- return expect(triggered).toBe(true);
+ expect(triggered).toBe(true);
});
- return it('triggers `focus`', function() {
- var focused;
- focused = false;
- $(this.selector).on('focus', function() {
- return focused = true;
- });
+ it('triggers `focus`', function() {
this.shortcut.replyWithSelectedText();
- return expect(focused).toBe(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
});
describe('with a one-line selection', function() {
- return it('quotes the selection', function() {
+ it('quotes the selection', function() {
stubSelection('<p>This text has been selected.</p>');
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
+ expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
});
});
- return describe('with a multi-line selection', function() {
- return it('quotes the selected lines as a group', function() {
+ describe('with a multi-line selection', function() {
+ it('quotes the selected lines as a group', function() {
stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
this.shortcut.replyWithSelectedText();
- return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
+ expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
});
});
});
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
index c274b9c45f4..d83d9a57b42 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require signin_tabs_memoizer */
+require('~/signin_tabs_memoizer');
((global) => {
describe('SigninTabsMemoizer', () => {
diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6
index 39d236986b9..4366ec2a5b8 100644
--- a/spec/javascripts/smart_interval_spec.js.es6
+++ b/spec/javascripts/smart_interval_spec.js.es6
@@ -1,5 +1,4 @@
-//= require jquery
-//= require smart_interval
+require('~/smart_interval');
(() => {
const DEFAULT_MAX_INTERVAL = 100;
@@ -164,7 +163,7 @@
const interval = this.smartInterval;
setTimeout(() => {
- $(document).trigger('page:before-unload');
+ $(document).triggerHandler('beforeunload');
expect(interval.state.intervalId).toBeUndefined();
expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
done();
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
deleted file mode 100644
index f8e3aca29fa..00000000000
--- a/spec/javascripts/spec_helper.js
+++ /dev/null
@@ -1,48 +0,0 @@
-/* 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.
-/*= require support/bind-poly */
-
-// You can require your own javascript files here. By default this will include
-// everything in application, however you may get better load performance if you
-// require the specific files that are being used in the spec that tests them.
-/*= require jquery */
-/*= require jquery.turbolinks */
-/*= require bootstrap */
-/*= require underscore */
-
-// Teaspoon includes some support files, but you can use anything from your own
-// support path too.
-// require support/jasmine-jquery-1.7.0
-// require support/jasmine-jquery-2.0.0
-/*= require support/jasmine-jquery-2.1.0 */
-
-// require support/sinon
-// require support/your-support-file
-// Deferring execution
-// If you're using CommonJS, RequireJS or some other asynchronous library you can
-// defer execution. Call Teaspoon.execute() after everything has been loaded.
-// Simple example of a timeout:
-// Teaspoon.defer = true
-// setTimeout(Teaspoon.execute, 1000)
-// Matching files
-// By default Teaspoon will look for files that match
-// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path
-// and it'll be included in the default suite automatically. If you want to
-// customize suites, check out the configuration in teaspoon_env.rb
-// Manifest
-// If you'd rather require your spec files manually (to control order for
-// instance) you can disable the suite matcher in the configuration and use this
-// file as a manifest.
-// For more information: http://github.com/modeset/teaspoon
-
-// set our fixtures path
-jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures';
-jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures';
-
-// defined in ActionDispatch::TestRequest
-// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7
-window.gl = window.gl || {};
-window.gl.TEST_HOST = 'http://test.host';
-window.gon = window.gon || {};
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
index 99f45850ea3..454386697f5 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -1,9 +1,6 @@
/* eslint-disable max-len, arrow-parens, comma-dangle */
-//= vue
-//= vue-resource
-//= require jquery
-//= require subbable_resource
+require('~/subbable_resource');
/*
* Test that each rest verb calls the publish and subscribe function and passes the correct value back
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 436f7064a69..c0c3837d1f4 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,6 +1,6 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
-/*= require syntax_highlight */
+require('~/syntax_highlight');
(function() {
describe('Syntax Highlighter', function() {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
new file mode 100644
index 00000000000..030d2de090a
--- /dev/null
+++ b/spec/javascripts/test_bundle.js
@@ -0,0 +1,44 @@
+// enable test fixtures
+require('jasmine-jquery');
+
+jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
+jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures';
+
+// include common libraries
+window.$ = window.jQuery = require('jquery');
+window._ = require('underscore');
+window.Cookies = require('js-cookie');
+window.Vue = require('vue');
+window.Vue.use(require('vue-resource'));
+require('jquery-ujs');
+require('bootstrap/js/affix');
+require('bootstrap/js/alert');
+require('bootstrap/js/button');
+require('bootstrap/js/collapse');
+require('bootstrap/js/dropdown');
+require('bootstrap/js/modal');
+require('bootstrap/js/scrollspy');
+require('bootstrap/js/tab');
+require('bootstrap/js/transition');
+require('bootstrap/js/tooltip');
+require('bootstrap/js/popover');
+
+// stub expected globals
+window.gl = window.gl || {};
+window.gl.TEST_HOST = 'http://test.host';
+window.gon = window.gon || {};
+
+// render all of our tests
+const testsContext = require.context('.', true, /_spec$/);
+testsContext.keys().forEach(function (path) {
+ try {
+ testsContext(path);
+ } catch (err) {
+ console.error('[ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function () {
+ it(`includes '${path}'`, function () {
+ expect(err).toBeNull();
+ });
+ });
+ }
+});
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 80163fd72d3..cba1af4daa4 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -2,11 +2,11 @@
/* global MockU2FDevice */
/* global U2FAuthenticate */
-/*= require u2f/authenticate */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/authenticate');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FAuthenticate', function() {
@@ -25,19 +25,20 @@
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form')
);
+
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+
return this.component.start();
});
it('allows authenticating via a U2F device', function() {
- var authenticatedMessage, deviceResponse, inProgressMessage;
+ var inProgressMessage;
inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
this.u2fDevice.respondToAuthenticateRequest({
deviceData: "this is data from the device"
});
- authenticatedMessage = this.container.find("p");
- deviceResponse = this.container.find('#js-device-response');
- expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.');
- return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
return describe("errors", function() {
it("displays an error message", function() {
@@ -51,7 +52,7 @@
return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
});
return it("allows retrying authentication after an error", function() {
- var authenticatedMessage, retryButton, setupButton;
+ var retryButton, setupButton;
setupButton = this.container.find("#js-login-u2f-device");
setupButton.trigger('click');
this.u2fDevice.respondToAuthenticateRequest({
@@ -64,8 +65,7 @@
this.u2fDevice.respondToAuthenticateRequest({
deviceData: "this is data from the device"
});
- authenticatedMessage = this.container.find("p");
- return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated.");
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
});
});
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 0790553b67e..10578c2c4b5 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -2,11 +2,11 @@
/* global MockU2FDevice */
/* global U2FRegister */
-/*= require u2f/register */
-/*= require u2f/util */
-/*= require u2f/error */
-/*= require u2f */
-/*= require ./mock_u2f_device */
+require('~/u2f/register');
+require('~/u2f/util');
+require('~/u2f/error');
+require('vendor/u2f');
+require('./mock_u2f_device');
(function() {
describe('U2FRegister', function() {
diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6
index b21f6912e06..9727c03c91e 100644
--- a/spec/javascripts/visibility_select_spec.js.es6
+++ b/spec/javascripts/visibility_select_spec.js.es6
@@ -1,4 +1,4 @@
-/*= require visibility_select */
+require('~/visibility_select');
(() => {
const VisibilitySelect = gl.VisibilitySelect;
diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6
index d6c6f786fb1..15ab10b9b69 100644
--- a/spec/javascripts/vue_common_components/commit_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6
@@ -1,4 +1,4 @@
-//= require vue_common_component/commit
+require('~/vue_shared/components/commit');
describe('Commit component', () => {
let props;
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
new file mode 100644
index 00000000000..412abfd5e41
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6
@@ -0,0 +1,87 @@
+require('~/vue_shared/components/pipelines_table_row');
+const pipeline = require('../../commit/pipelines/mock_data');
+
+describe('Pipelines Table Row', () => {
+ let component;
+ preloadFixtures('static/environments/element.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+
+ component = new gl.pipelines.PipelinesTableRowComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipeline,
+ svgs: {},
+ },
+ });
+ });
+
+ it('should render a table row', () => {
+ expect(component.$el).toEqual('TR');
+ });
+
+ describe('status column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render status text', () => {
+ expect(
+ component.$el.querySelector('td.commit-link a').textContent,
+ ).toContain(pipeline.details.status.text);
+ });
+ });
+
+ describe('information column', () => {
+ it('should render a pipeline link', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
+ ).toEqual(pipeline.path);
+ });
+
+ it('should render pipeline ID', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a > span').textContent,
+ ).toEqual(`#${pipeline.id}`);
+ });
+
+ describe('when a user is provided', () => {
+ it('should render user information', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ ).toEqual(pipeline.user.web_url);
+
+ expect(
+ component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
+ ).toEqual(pipeline.user.name);
+ });
+ });
+ });
+
+ describe('commit column', () => {
+ it('should render link to commit', () => {
+ expect(
+ component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
+ ).toEqual(pipeline.commit.commit_path);
+ });
+ });
+
+ describe('stages column', () => {
+ it('should render an icon for each stage', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
+ ).toEqual(pipeline.details.stages.length);
+ });
+ });
+
+ describe('actions column', () => {
+ it('should render the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('td:nth-child(6) ul li').length,
+ ).toEqual(pipeline.details.manual_actions.length);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
new file mode 100644
index 00000000000..54d81e2ea7d
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6
@@ -0,0 +1,64 @@
+require('~/vue_shared/components/pipelines_table');
+require('~/lib/utils/datetime_utility');
+const pipeline = require('../../commit/pipelines/mock_data');
+
+describe('Pipelines Table', () => {
+ preloadFixtures('static/environments/element.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/environments/element.html.raw');
+ });
+
+ describe('table', () => {
+ let component;
+ beforeEach(() => {
+ component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [],
+ svgs: {},
+ },
+ });
+ });
+
+ it('should render a table', () => {
+ expect(component.$el).toEqual('TABLE');
+ });
+
+ it('should render table head with correct columns', () => {
+ expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status');
+ expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline');
+ expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit');
+ expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages');
+ expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual('');
+ expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual('');
+ });
+ });
+
+ describe('without data', () => {
+ it('should render an empty table', () => {
+ const component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [],
+ svgs: {},
+ },
+ });
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
+ });
+ });
+
+ describe('with data', () => {
+ it('should render rows', () => {
+ const component = new gl.pipelines.PipelinesTableComponent({
+ el: document.querySelector('.test-dom-element'),
+ propsData: {
+ pipelines: [pipeline],
+ svgs: {},
+ },
+ });
+
+ expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
index 1a7f2bb5fb8..e84f0dcfe67 100644
--- a/spec/javascripts/vue_pagination/pagination_spec.js.es6
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6
@@ -1,7 +1,5 @@
-//= require vue
-//= require lib/utils/common_utils
-//= require vue_pagination/index
-/* global fixture, gl */
+require('~/lib/utils/common_utils');
+require('~/vue_shared/components/table_pagination');
describe('Pagination component', () => {
let component;
@@ -17,7 +15,7 @@ describe('Pagination component', () => {
};
it('should render and start at page 1', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
@@ -40,7 +38,7 @@ describe('Pagination component', () => {
});
it('should go to the previous page', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
@@ -61,7 +59,7 @@ describe('Pagination component', () => {
});
it('should go to the next page', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
@@ -82,7 +80,7 @@ describe('Pagination component', () => {
});
it('should go to the last page', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
@@ -103,7 +101,7 @@ describe('Pagination component', () => {
});
it('should go to the first page', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
@@ -124,7 +122,7 @@ describe('Pagination component', () => {
});
it('should do nothing', () => {
- fixture.set('<div class="test-pagination-container"></div>');
+ setFixtures('<div class="test-pagination-container"></div>');
component = new window.gl.VueGlPagination({
el: document.querySelector('.test-pagination-container'),
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index be706ca304f..ce33a6814aa 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -3,7 +3,7 @@
/* global Mousetrap */
/* global ZenMode */
-/*= require zen_mode */
+require('~/zen_mode');
(function() {
var enterZen, escapeKeydown, exitZen;
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index 81b9a513ce3..deaabceef1c 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do
it 'returns the referenced project' do
project2 = double('referenced project')
- expect(Project).to receive(:find_with_namespace).
+ expect(Project).to receive(:find_by_full_path).
with('cross/reference').and_return(project2)
expect(project_from_ref('cross/reference')).to eq project2
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
new file mode 100644
index 00000000000..f85a5dcbd8b
--- /dev/null
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Banzai::Filter::PlantumlFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'should replace plantuml pre tag with img tag' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if disabled' do
+ stub_application_setting(plantuml_enabled: false)
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<pre class="plantuml"><code>Bob -&gt; Sara : Hello</code><pre></pre></pre>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+
+ it 'should not replace plantuml pre tag with img tag if url is invalid' do
+ stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid")
+ input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>'
+ output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
+ doc = filter(input)
+
+ expect(doc.to_s).to eq output
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index f824e2e1efe..68ad429608d 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -4,6 +4,34 @@ module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
+ describe 'our current .gitlab-ci.yml' do
+ let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
+
+ it 'is valid' do
+ error_message = described_class.validation_message(config)
+
+ expect(error_message).to be_nil
+ end
+ end
+
+ describe '#build_attributes' do
+ describe 'coverage entry' do
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+
+ describe 'code coverage regexp' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ coverage: '/Code coverage: \d+\.\d+/' })
+ end
+
+ it 'includes coverage regexp in build attributes' do
+ expect(subject)
+ .to include(coverage_regex: 'Code coverage: \d+\.\d+')
+ end
+ end
+ end
+ end
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -21,6 +49,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {},
allow_failure: false,
@@ -435,6 +464,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -463,6 +493,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.5",
@@ -702,6 +733,7 @@ module Ci
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
+ coverage_regex: nil,
tag_list: [],
options: {
image: "ruby:2.1",
@@ -913,6 +945,7 @@ module Ci
stage_idx: 1,
name: "normal_job",
commands: "test",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -958,6 +991,7 @@ module Ci
stage_idx: 0,
name: "job1",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
@@ -970,6 +1004,7 @@ module Ci
stage_idx: 0,
name: "job2",
commands: "execute-script-for-job",
+ coverage_regex: nil,
tag_list: [],
options: {},
when: "on_success",
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index e3066311b7d..d70690f589d 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -5,15 +5,15 @@ describe EventFilter, lib: true do
let(:source_user) { create(:user) }
let!(:public_project) { create(:empty_project, :public) }
- let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
- let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
- let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) }
- let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) }
- let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) }
- let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) }
- let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
- let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
- let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+ let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
+ let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
+ let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
+ let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
+ let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) }
+ let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) }
+ let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) }
+ let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) }
+ let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) }
it 'applies push filter' do
events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f251c0dd25a..b234de4c772 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
end
- it 'recognizes user lfs tokens' do
- user = create(:user)
- token = Gitlab::LfsToken.new(user).token
+ context 'while using LFS authenticate' do
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ token = Gitlab::LfsToken.new(user).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
- expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+ end
- it 'recognizes deploy key lfs tokens' do
- key = create(:deploy_key)
- token = Gitlab::LfsToken.new(key).token
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ token = Gitlab::LfsToken.new(key).token
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
- end
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ end
- context "while using OAuth tokens as passwords" do
- it 'succeeds for OAuth tokens with the `api` scope' do
+ it 'does not try password auth before oauth' do
user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
+ token = Gitlab::LfsToken.new(user).token
+
+ expect(gl_auth).not_to receive(:find_with_user_password)
+ gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using OAuth tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
+ let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) }
+
+ it 'succeeds for OAuth tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
- expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
end
it 'fails for OAuth tokens with other scopes' do
- user = create(:user)
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
+ token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user')
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before oauth' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')
+ end
end
- context "while using personal access tokens as passwords" do
- it 'succeeds for personal access tokens with the `api` scope' do
- user = create(:user)
- personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
+ context 'while using personal access tokens as passwords' do
+ let(:user) { create(:user) }
+ let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) }
+ it 'succeeds for personal access tokens with the `api` scope' do
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
- expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+ expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
end
it 'fails for personal access tokens with other scopes' do
- user = create(:user)
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
end
+
+ it 'does not try password auth before personal access tokens' do
+ expect(gl_auth).not_to receive(:find_with_user_password)
+
+ gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')
+ end
+ end
+
+ context 'while using regular user and password' do
+ it 'falls through lfs authentication' do
+ user = create(
+ :user,
+ username: 'normal_user',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
+
+ it 'falls through oauth authentication when the username is oauth2' do
+ user = create(
+ :user,
+ username: 'oauth2',
+ password: 'my-secret',
+ )
+
+ expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+ end
end
it 'returns double nil for invalid credentials' do
diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb
index 1e81eaef18c..b6e924d67be 100644
--- a/spec/lib/gitlab/chat_commands/command_spec.rb
+++ b/spec/lib/gitlab/chat_commands/command_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::ChatCommands::Command, service: true do
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 start_with('Unknown command')
expect(subject[:text]).to match('/gitlab issue show')
end
end
@@ -34,47 +34,7 @@ describe Gitlab::ChatCommands::Command, service: true do
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
-
- context 'searching for an issue' do
- let(:params) { { text: 'issue search find me' } }
- let!(:issue) { create(:issue, project: project, title: 'find me') }
-
- before do
- project.team << [user, :master]
- end
-
- context 'a single issue is found' do
- it 'presents the issue' do
- expect(subject[:text]).to match(issue.title)
- end
- end
-
- context 'multiple issues found' do
- let!(:issue2) { create(:issue, project: project, title: "someone find me") }
-
- it 'shows a link to the new issue' do
- expect(subject[:text]).to match(issue.title)
- expect(subject[:text]).to match(issue2.title)
- end
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -90,7 +50,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
- expect(subject[:text]).to start_with('Whoops! That action is not allowed')
+ expect(subject[:text]).to start_with('Whoops! This action is not allowed')
end
end
@@ -100,7 +60,7 @@ describe Gitlab::ChatCommands::Command, service: true do
end
it 'returns action' do
- expect(subject[:text]).to include('Deployment from staging to production started.')
+ expect(subject[:text]).to include('Deployment started from staging to production')
expect(subject[:response_type]).to be(:in_channel)
end
@@ -130,7 +90,7 @@ describe Gitlab::ChatCommands::Command, service: true do
context 'IssueCreate is triggered' do
let(:params) { { text: 'issue create my title' } }
- it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) }
+ it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) }
end
context 'IssueSearch is triggered' do
diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb
index bd8099c92da..b3358a32161 100644
--- a/spec/lib/gitlab/chat_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb
@@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
context 'if no environment is defined' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
- it 'returns nil' do
- expect(subject).to be_nil
+ it 'does not execute an action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
end
end
@@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
context 'when duplicate action exists' do
@@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do
end
it 'returns error' do
- expect(subject.type).to eq(:error)
- expect(subject.message).to include('Too many actions defined')
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq('Too many actions defined')
end
end
@@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do
name: 'teardown', environment: 'production')
end
- it 'returns success result' do
- expect(subject.type).to eq(:success)
- expect(subject.message).to include('Deployment from staging to production started')
+ it 'returns the success message' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with('Deployment started from staging to production')
end
end
end
diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
index 6c71e79ff6d..84c22328064 100644
--- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::ChatCommands::IssueCreate, service: true do
+describe Gitlab::ChatCommands::IssueNew, service: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
it 'creates the issue' do
expect { subject }.to change { project.issues.count }.by(1)
- expect(subject.title).to eq('bird is the word')
+ expect(subject[:response_type]).to be(:in_channel)
end
end
@@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do
expect { subject }.to change { project.issues.count }.by(1)
end
end
+
+ context 'issue cannot be created' do
+ let!(:issue) { create(:issue, project: project, title: 'bird is the word') }
+ let(:regex_match) { described_class.match("issue create #{'a' * 512}}") }
+
+ it 'displays the errors' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("- Title is too long")
+ end
+ end
end
describe '.match' do
diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
index 24c06a967fa..551ccb79a58 100644
--- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueSearch, service: true do
describe '#execute' do
- let!(:issue) { create(:issue, title: 'find me') }
+ let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
- let(:project) { issue.project }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue search find") }
@@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
context 'when the user has no access' do
it 'only returns the open issues' do
- expect(subject).not_to include(confidential)
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
@@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do
end
it 'returns all results' do
- expect(subject).to include(confidential, issue)
+ expect(subject).to have_key(:attachments)
+ expect(subject[:text]).to eq("Here are the 2 issues I found:")
end
end
context 'without hits on the query' do
it 'returns an empty collection' do
- expect(subject).to be_empty
+ expect(subject[:text]).to match("not found")
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
index 2eab73e49e5..1f20d0a44ce 100644
--- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe Gitlab::ChatCommands::IssueShow, service: true do
describe '#execute' do
- let(:issue) { create(:issue) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, project: project) }
+ let(:project) { create(:empty_project) }
let(:user) { issue.author }
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
@@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
end
context 'the issue exists' do
+ let(:title) { subject[:attachments].first[:title] }
+
it 'returns the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
context 'when its reference is given' do
let(:regex_match) { described_class.match("issue show #{issue.to_reference}") }
it 'shows the issue' do
- expect(subject.iid).to be issue.iid
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(title).to start_with(issue.title)
end
end
end
@@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do
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
+ it "returns not found" do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to match("not found")
end
end
end
- describe 'self.match' do
+ describe '.match' do
it 'matches the iid' do
match = described_class.match("issue show 123")
expect(match[:iid]).to eq("123")
end
+
+ it 'accepts a reference' do
+ match = described_class.match("issue show #{Issue.reference_prefix}123")
+
+ expect(match[:iid]).to eq("123")
+ end
end
end
diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
new file mode 100644
index 00000000000..ae41d75ab0c
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Access do
+ describe '#access_denied' do
+ subject { described_class.new.access_denied }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'displays an error message' do
+ expect(subject[:text]).to match("is not allowed")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#not_found' do
+ subject { described_class.new.not_found }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user the resource was not found' do
+ expect(subject[:text]).to match("not found!")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ describe '#authorize' do
+ context 'with an authorization URL' do
+ subject { described_class.new('http://authorize.me').authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("connect your GitLab account")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+
+ context 'without authorization url' do
+ subject { described_class.new.authorize }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'tells the user to authorize' do
+ expect(subject[:text]).to match("Couldn't identify you")
+ expect(subject[:response_type]).to be(:ephemeral)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
new file mode 100644
index 00000000000..dc2dd300072
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::Deploy do
+ let(:build) { create(:ci_build) }
+
+ describe '#present' do
+ subject { described_class.new(build).present('staging', 'prod') }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'messages the channel of the deploy' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject[:text]).to start_with("Deployment started from staging to prod")
+ end
+ end
+
+ describe '#no_actions' do
+ subject { described_class.new(nil).no_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("No action found to be executed")
+ end
+ end
+
+ describe '#too_many_actions' do
+ subject { described_class.new([]).too_many_actions }
+
+ it { is_expected.to have_key(:text) }
+ it { is_expected.to have_key(:response_type) }
+ it { is_expected.to have_key(:status) }
+ it { is_expected.not_to have_key(:attachments) }
+
+ it 'tells the user there is no action' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to eq("Too many actions defined")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
new file mode 100644
index 00000000000..17fcdbc2452
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueNew do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
new file mode 100644
index 00000000000..ec6d3e34a96
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueSearch do
+ let(:project) { create(:empty_project) }
+ let(:message) { subject[:text] }
+
+ before { create_list(:issue, 2, project: project) }
+
+ subject { described_class.new(project.issues).present }
+
+ it 'formats the message correct' do
+ is_expected.to have_key(:text)
+ is_expected.to have_key(:status)
+ is_expected.to have_key(:response_type)
+ is_expected.to have_key(:attachments)
+ end
+
+ it 'shows a list of results' do
+ expect(subject[:response_type]).to be(:ephemeral)
+
+ expect(message).to start_with("Here are the 2 issues I found")
+ end
+end
diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
new file mode 100644
index 00000000000..5b678d31fce
--- /dev/null
+++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ChatCommands::Presenters::IssueShow do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:attachment) { subject[:attachments].first }
+
+ subject { described_class.new(issue).present }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the issue' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(subject).to have_key(:attachments)
+ expect(attachment[:title]).to start_with(issue.title)
+ end
+
+ context 'with upvotes' do
+ before do
+ create(:award_emoji, :upvote, awardable: issue)
+ end
+
+ it 'shows the upvote count' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open** · :+1: 1")
+ end
+ end
+
+ context 'confidential issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'shows an ephemeral response' do
+ expect(subject[:response_type]).to be(:in_channel)
+ expect(attachment[:text]).to start_with("**Open**")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
new file mode 100644
index 00000000000..4c6bd859552
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Coverage do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context "when entry config value doesn't have the surrounding '/'" do
+ let(:config) { 'Code coverage: \d+\.\d+' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ context "when entry config value has the surrounding '/'" do
+ let(:config) { '/Code coverage: \d+\.\d+/' }
+
+ describe '#value' do
+ subject { entry.value }
+ it { is_expected.to eq(config[1...-1]) }
+ end
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to be_empty }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { '(malformed regexp' }
+
+ describe '#errors' do
+ subject { entry.errors }
+ it { is_expected.to include(/coverage config must be a regular expression/) }
+ end
+
+ describe '#valid?' do
+ subject { entry }
+ it { is_expected.not_to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index e64c8d46bd8..432a99dce33 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -4,12 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do
let(:global) { described_class.new(hash) }
describe '.nodes' do
- it 'can contain global config keys' do
- expect(described_class.nodes).to include :before_script
+ it 'returns a hash' do
+ expect(described_class.nodes).to be_a(Hash)
end
- it 'returns a hash' do
- expect(described_class.nodes).to be_a Hash
+ context 'when filtering all the entry/node names' do
+ it 'contains the expected node names' do
+ expect(described_class.nodes.keys)
+ .to match_array(%i[before_script image services
+ after_script variables stages
+ types cache])
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index fc9b8b86dc4..d20f4ec207d 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -3,6 +3,20 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
+ describe '.nodes' do
+ context 'when filtering all the entry/node names' do
+ subject { described_class.nodes.keys }
+
+ let(:result) do
+ %i[before_script script stage type after_script cache
+ image services only except variables artifacts
+ environment coverage]
+ end
+
+ it { is_expected.to match_array result }
+ end
+ end
+
describe 'validations' do
before { entry.compose! }
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index b3c07347de1..8ad9b7cdf07 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Cancelable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'ban' }
+ it { expect(subject.action_icon).to eq 'icon_action_cancel' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f1b50a59ce6..f3e72ea1796 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::Ci::Status::Build::Play do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'play' }
+ it { expect(subject.action_icon).to eq 'icon_action_play' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 62036f8ec5d..2db0f8d29bd 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Status::Build::Retryable do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'refresh' }
+ it { expect(subject.action_icon).to eq 'icon_action_retry' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 597e02e86e4..41c2b624774 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::Ci::Status::Build::Stop do
end
describe '#action_icon' do
- it { expect(subject.action_icon).to eq 'stop' }
+ it { expect(subject.action_icon).to eq 'icon_action_stop' }
end
describe '#action_title' do
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 01b2a55b63c..e18a219ef36 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::ContributionsCalendar do
end
let(:feature_project) do
- create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project|
+ create(:empty_project, :public, :issues_private) do |project|
create(:project_member, user: contributor, project: project).project
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
new file mode 100644
index 00000000000..c455cd9b942
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::BaseEventFetcher do
+ let(:max_events) { 2 }
+ let(:project) { create(:project) }
+ let(:user) { create(:user, :admin) }
+ let(:start_time_attrs) { Issue.arel_table[:created_at] }
+ let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] }
+ let(:options) do
+ { start_time_attrs: start_time_attrs,
+ end_time_attrs: end_time_attrs,
+ from: 30.days.ago }
+ end
+
+ subject do
+ described_class.new(project: project,
+ stage: :issue,
+ options: options).fetch
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return(Issue.all)
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:serialize) do |event|
+ event
+ end
+
+ stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events)
+
+ setup_events(count: 3)
+ end
+
+ it 'limits the rows to the max number' do
+ expect(subject.count).to eq(max_events)
+ end
+
+ def setup_events(count:)
+ count.times do
+ issue = create(:issue, project: project, created_at: 2.days.ago)
+ milestone = create(:milestone, project: project)
+
+ issue.update(milestone: milestone)
+ create_merge_request_closing_issue(issue)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 7fd25b9e5bf..e94ca4fcfd2 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -12,15 +12,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
describe '#add_concurrent_index' do
context 'outside a transaction' do
before do
- expect(model).to receive(:transaction_open?).and_return(false)
-
- unless Gitlab::Database.postgresql?
- allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout)
- end
+ allow(model).to receive(:transaction_open?).and_return(false)
end
context 'using PostgreSQL' do
- before { expect(Gitlab::Database).to receive(:postgresql?).and_return(true) }
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(model).to receive(:disable_statement_timeout)
+ end
it 'creates the index concurrently' do
expect(model).to receive(:add_index).
@@ -59,6 +58,71 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#add_concurrent_foreign_key' do
+ context 'inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end.to raise_error(RuntimeError)
+ end
+ end
+
+ context 'outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using MySQL' do
+ it 'creates a regular foreign key' do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+
+ expect(model).to receive(:add_foreign_key).
+ with(:projects, :users, column: :user_id, on_delete: :cascade)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
+ end
+
+ context 'using PostgreSQL' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ it 'creates a concurrent foreign key' do
+ expect(model).to receive(:disable_statement_timeout)
+ expect(model).to receive(:execute).ordered.with(/NOT VALID/)
+ expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
+ end
+ end
+ end
+
+ describe '#disable_statement_timeout' do
+ context 'using PostgreSQL' do
+ it 'disables statement timeouts' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(model).to receive(:execute).with('SET statement_timeout TO 0')
+
+ model.disable_statement_timeout
+ end
+ end
+
+ context 'using MySQL' do
+ it 'does nothing' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).not_to receive(:execute)
+
+ model.disable_statement_timeout
+ end
+ end
+ end
+
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 3031559c613..b142b3a2781 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do
end
end
+ describe '.nulls_first_order' do
+ context 'when using PostgreSQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(true) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'}
+ end
+
+ context 'when using MySQL' do
+ before { expect(described_class).to receive(:postgresql?).and_return(false) }
+
+ it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'}
+ it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'}
+ end
+ end
+
describe '#true_value' do
it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true)
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 1e21270d928..5893485634d 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with a diff file" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
@@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do
context "with diff lines" do
let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight }
- it 'should return Gitlab::Diff::Line elements' do
+ it 'returns Gitlab::Diff::Line elements' do
expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line)
end
- it 'should not modify "match" lines' do
+ it 'does not modify "match" lines' do
expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen')
expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen')
end
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index fe5fa048413..0f779339c54 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
subject { described_class.new(diff_file) }
describe '#parallelize' do
- it 'should return an array of arrays containing the parsed diff' do
+ it 'returns an array of arrays containing the parsed diff' do
diff_lines = diff_file.highlighted_diff_lines
expected = [
# Unchanged lines
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index f5822fed37c..994995b57b8 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::CreateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Create file",
file_path: file_name,
@@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do
Files::UpdateService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Update file",
file_path: file_name,
@@ -122,10 +122,10 @@ describe Gitlab::Diff::PositionTracer, lib: true do
end
def delete_file(branch_name, file_name)
- Files::DeleteService.new(
+ Files::DestroyService.new(
project,
current_user,
- source_branch: branch_name,
+ start_branch: branch_name,
target_branch: branch_name,
commit_message: "Delete file",
file_path: file_name
@@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do
}
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request, options)
+
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 17a4ef25210..b300feaabe1 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
it_behaves_like 'an email that contains a mail key', 'References'
end
+
+ context 'mail key is in the References header with a comma' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') }
+
+ it_behaves_like 'an email that contains a mail key', 'References'
+ end
end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index b080be62b34..a55bd4387e0 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -94,8 +94,6 @@ describe Gitlab::GitAccess, lib: true do
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
@@ -209,7 +207,13 @@ describe Gitlab::GitAccess, lib: true do
stub_git_hooks
project.repository.add_branch(user, unprotected_branch, 'feature')
target_branch = project.repository.lookup('feature')
- source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false)
+ source_branch = project.repository.commit_file(
+ user,
+ FFaker::InternetSE.login_user_name,
+ FFaker::HipsterIpsum.paragraph,
+ message: FFaker::HipsterIpsum.sentence,
+ branch_name: unprotected_branch,
+ update: false)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4a0cdc6887e..1ae293416e4 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -36,8 +36,6 @@ describe Gitlab::GitAccessWiki, lib: true do
context 'when wiki feature is enabled' do
it 'give access to download wiki code' do
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
-
expect(subject.allowed?).to be_truthy
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 72421832ffc..afd78abdc9b 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -202,7 +202,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
end
end
- let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) }
+ let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") }
let(:credentials) { { user: 'joe' } }
context 'when importing a GitHub project' do
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index fadfe4d378e..e177d883158 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do
Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb')
end
- it 'should properly highlight all the lines' do
+ it 'highlights all the lines properly' do
expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7fb6829f582..06617f3b007 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -52,6 +52,7 @@ snippets:
- project
- notes
- award_emoji
+- user_agent_detail
releases:
- project
project_members:
@@ -191,6 +192,7 @@ project:
- environments
- deployments
- project_feature
+- pages_domains
- authorized_users
- project_authorizations
- route
@@ -201,5 +203,6 @@ award_emoji:
priorities:
- label
timelogs:
-- trackable
+- issue
+- merge_request
- user
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 0b7984d6ca9..b9d4e59e770 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -92,5 +92,51 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.map[exported_user_id]).to eq(user2.id)
end
end
+
+ context 'importer same as group member' do
+ let(:user2) { create(:admin, authorized_projects_populated: true) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user2, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ end
+
+ it 'maps the project member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+
+ it 'maps the project member if it already exists' do
+ project.add_master(user2)
+
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
+
+ context 'importing group members' do
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:members_mapper) do
+ described_class.new(
+ exported_members: exported_members, user: user, project: project)
+ end
+
+ before do
+ group.add_users([user, user2], GroupMember::DEVELOPER)
+ user.update(email: 'invite@test.com')
+ end
+
+ it 'maps the importer' do
+ expect(members_mapper.map[-1]).to eq(user.id)
+ end
+
+ it 'maps the group member' do
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 2c0750c3377..2e9f60432b4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6981,11 +6981,16 @@
]
}
],
- "variables": [
-
- ],
"triggers": [
-
+ {
+ "id": 123,
+ "token": "cdbfasdf44a5958c83654733449e585",
+ "project_id": null,
+ "deleted_at": null,
+ "created_at": "2017-01-16T15:25:28.637Z",
+ "updated_at": "2017-01-16T15:25:28.637Z",
+ "gl_project_id": 123
+ }
],
"deploy_keys": [
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 4b07fa53bf5..0af13ba8e47 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
let(:user) { create(:user) }
let(:namespace) { create(:namespace, owner: user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
- let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) }
+ let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
@@ -121,13 +121,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end
context 'with group' do
- let!(:project) do
+ let!(:project) do
create(:empty_project,
- name: 'project',
- path: 'project',
- builds_access_level: ProjectFeature::DISABLED,
- issues_access_level: ProjectFeature::DISABLED,
- group: create(:group))
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
end
it 'has group labels' do
@@ -197,6 +197,20 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(restored_project_json).to be true
end
end
+
+ context 'tokens are regenerated' do
+ before do
+ restored_project_json
+ end
+
+ it 'has a new CI trigger token' do
+ expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
+ end
+
+ it 'has a new CI build token' do
+ expect(Ci::Build.where(token: 'abcd')).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index d480c3821ec..3628adefc0c 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
describe 'saves the project tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
- let(:project_tree_saver) { described_class.new(project: project, shared: shared) }
+ let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
@@ -92,7 +92,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has pipeline builds' do
- expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build'}).to eq(1)
+ expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1)
end
it 'has pipeline commits' do
@@ -112,13 +112,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
end
it 'has project and group labels' do
- label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type']}
+ label_types = saved_project_json['issues'].first['label_links'].map { |link| link['label']['type'] }
expect(label_types).to match_array(['ProjectLabel', 'GroupLabel'])
end
it 'has priorities associated to labels' do
- priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities']}
+ priorities = saved_project_json['issues'].first['label_links'].map { |link| link['label']['priorities'] }
expect(priorities.flatten).not_to be_empty
end
@@ -140,6 +140,51 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(project_tree_saver.save).to be true
end
+
+ context 'group members' do
+ let(:user2) { create(:user, email: 'group@member.com') }
+ let(:member_emails) do
+ saved_project_json['project_members'].map do |pm|
+ pm['user']['email']
+ end
+ end
+
+ before do
+ Group.first.add_developer(user2)
+ end
+
+ it 'does not export group members if it has no permission' do
+ Group.first.add_developer(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'does not export group members as master' do
+ Group.first.add_master(user)
+
+ expect(member_emails).not_to include('group@member.com')
+ end
+
+ it 'exports group members as group owner' do
+ Group.first.add_owner(user)
+
+ expect(member_emails).to include('group@member.com')
+ end
+
+ context 'as admin' do
+ let(:user) { create(:admin) }
+
+ it 'exports group members as admin' do
+ expect(member_emails).to include('group@member.com')
+ end
+
+ it 'exports group members as project members' do
+ member_types = saved_project_json['project_members'].map { |pm| pm['source_type'] }
+
+ expect(member_types).to all(eq('Project'))
+ end
+ end
+ end
end
end
@@ -152,6 +197,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project = create(:project,
:public,
:repository,
+ :issues_disabled,
+ :wiki_enabled,
+ :builds_private,
issues: [issue],
snippets: [snippet],
releases: [release],
@@ -167,10 +215,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_status = create(:commit_status, project: project)
ci_pipeline = create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- statuses: [commit_status])
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
create(:ci_build, pipeline: ci_pipeline, project: project)
create(:milestone, project: project)
@@ -182,13 +230,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
- project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE)
-
project
end
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index 3ceb1e7e803..48d74b07e27 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -86,6 +86,10 @@ describe Gitlab::ImportExport::Reader, lib: true do
expect(described_class.new(shared: shared).project_tree).to match(include: [{ issues: { methods: [:name] } }])
end
+ it 'generates the correct hash for group members' do
+ expect(described_class.new(shared: shared).group_members_tree).to match({ include: { user: { only: [:email] } } })
+ end
+
def setup_yaml(hash)
allow(YAML).to receive(:load_file).with(test_config).and_return(hash)
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index db0084d6823..57e412b0cef 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -55,8 +55,8 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
expect(created_object.project_id).to eq(project.id)
end
- it 'has a token' do
- expect(created_object.token).to eq(token)
+ it 'has a nil token' do
+ expect(created_object.token).to eq(nil)
end
context 'original service exists' do
@@ -178,4 +178,15 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
expect(created_object.author).to eq(new_user)
end
end
+
+ context 'encrypted attributes' do
+ let(:relation_sym) { 'Ci::Variable' }
+ let(:relation_hash) do
+ create(:ci_variable).as_json
+ end
+
+ it 'has no value for the encrypted attribute' do
+ expect(created_object.value).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 493bc2db21a..c5ac702d831 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -222,6 +222,7 @@ CommitStatus:
- queued_at
- token
- lock_version
+- coverage_regex
Ci::Variable:
- id
- project_id
@@ -349,8 +350,8 @@ LabelPriority:
Timelog:
- id
- time_spent
-- trackable_id
-- trackable_type
+- merge_request_id
+- issue_id
- user_id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 7e951e3fcdd..698bd72d0f8 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -90,4 +90,19 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
end
end
+
+ context 'self.scan_fallback_references' do
+ let(:references) do
+ '<issue_1@localhost>' +
+ ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' +
+ ',<exchange@microsoft.com>'
+ end
+
+ it 'returns reply key' do
+ expect(described_class.scan_fallback_references(references))
+ .to eq(%w[issue_1@localhost
+ reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost
+ exchange@microsoft.com])
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index b9d12c3c24c..9dd997aa7dc 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
- it 'should block user in GitLab' do
+ it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 92e3624a8d8..9a8096208db 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -163,7 +163,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list issue notes when access is restricted" do
- project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :issues_private)
note = create(:note_on_issue, project: project)
results = described_class.new(user, project, note.note)
@@ -172,7 +172,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
end
it "doesn't list merge_request notes when access is restricted" do
- project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+ project = create(:empty_project, :public, :merge_requests_private)
note = create(:note_on_merge_request, project: project)
results = described_class.new(user, project, note.note)
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb
index 4092f7fb638..e2d6b1b9ab7 100644
--- a/spec/lib/gitlab/uploads_transfer_spec.rb
+++ b/spec/lib/gitlab/project_transfer_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
-describe Gitlab::UploadsTransfer, lib: true do
+describe Gitlab::ProjectTransfer, lib: true do
before do
@root_dir = File.join(Rails.root, "public", "uploads")
- @upload_transfer = Gitlab::UploadsTransfer.new
+ @project_transfer = Gitlab::ProjectTransfer.new
+ allow(@project_transfer).to receive(:root_dir).and_return(@root_dir)
@project_path_was = "test_project_was"
@project_path = "test_project"
@@ -21,7 +22,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#move_project' do
it "moves project upload to another namespace" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
+ @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
@@ -31,7 +32,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#rename_project' do
it "renames project" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was))
- @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
+ @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
@@ -41,7 +42,7 @@ describe Gitlab::UploadsTransfer, lib: true do
describe '#rename_namespace' do
it "renames namespace" do
FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path))
- @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path)
+ @project_transfer.rename_namespace(@namespace_path_was, @namespace_path)
expected_path = File.join(@root_dir, @namespace_path, @project_path)
expect(Dir.exist?(expected_path)).to be_truthy
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 6b689c41ef6..84cfd934fa0 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -42,14 +42,85 @@ describe Gitlab::ReferenceExtractor, lib: true do
> @offteam
})
+
expect(subject.users).to match_array([])
end
+ describe 'directly addressed users' do
+ before do
+ @u_foo = create(:user, username: 'foo')
+ @u_foo2 = create(:user, username: 'foo2')
+ @u_foo3 = create(:user, username: 'foo3')
+ @u_foo4 = create(:user, username: 'foo4')
+ @u_foo5 = create(:user, username: 'foo5')
+
+ @u_bar = create(:user, username: 'bar')
+ @u_bar2 = create(:user, username: 'bar2')
+ @u_bar3 = create(:user, username: 'bar3')
+ @u_bar4 = create(:user, username: 'bar4')
+
+ @u_tom = create(:user, username: 'tom')
+ @u_tom2 = create(:user, username: 'tom2')
+ end
+
+ context 'when a user is directly addressed' do
+ it 'accesses the user object which is mentioned in the beginning of the line' do
+ subject.analyze('@foo What do you think? cc: @bar, @tom')
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo])
+ end
+
+ it "doesn't access the user object if it's not mentioned in the beginning of the line" do
+ subject.analyze('What do you think? cc: @bar')
+
+ expect(subject.directly_addressed_users).to be_empty
+ end
+ end
+
+ context 'when multiple users are addressed' do
+ it 'accesses the user objects which are mentioned in the beginning of the line' do
+ subject.analyze('@foo @bar What do you think? cc: @tom')
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo, @u_bar])
+ end
+
+ it "doesn't access the user objects if they are not mentioned in the beginning of the line" do
+ subject.analyze('What do you think? cc: @foo @bar @tom')
+
+ expect(subject.directly_addressed_users).to be_empty
+ end
+ end
+
+ context 'when multiple users are addressed in different paragraphs' do
+ it 'accesses user objects which are mentioned in the beginning of each paragraph' do
+ subject.analyze <<-NOTE.strip_heredoc
+ @foo What do you think? cc: @tom
+
+ - @bar can you please have a look?
+
+ >>>
+ @foo2 what do you think? cc: @bar2
+ >>>
+
+ @foo3 @foo4 thank you!
+
+ > @foo5 well done!
+
+ 1. @bar3 Can you please check? cc: @tom2
+ 2. @bar4 What do you this of this MR?
+ NOTE
+
+ expect(subject.directly_addressed_users).to match_array([@u_foo, @u_foo3, @u_foo4])
+ end
+ end
+ end
+
it 'accesses valid issue objects' do
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.")
+
expect(subject.issues).to match_array([@i0, @i1])
end
@@ -58,6 +129,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict')
subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.")
+
expect(subject.merge_requests).to match_array([@m1, @m0])
end
@@ -67,6 +139,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@l2 = create(:label)
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
+
expect(subject.labels).to match_array([@l0, @l1])
end
@@ -76,6 +149,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
@s2 = create(:project_snippet)
subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
+
expect(subject.snippets).to match_array([@s0, @s1])
end
@@ -127,6 +201,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
it 'handles project issue references' do
subject.analyze("this refers issue #{issue.to_reference(project)}")
+
extracted = subject.issues
expect(extracted.size).to eq(1)
expect(extracted).to match_array([issue])
diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb
new file mode 100644
index 00000000000..2370f56a613
--- /dev/null
+++ b/spec/lib/gitlab/route_map_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Gitlab::RouteMap, lib: true do
+ describe '#initialize' do
+ context 'when the data is not YAML' do
+ it 'raises an error' do
+ expect { described_class.new('"') }.
+ to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/)
+ end
+ end
+
+ context 'when the data is not a YAML array' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump('foo')) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /an array/)
+ end
+ end
+
+ context 'when an entry is not a hash' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump(['foo'])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /a hash/)
+ end
+ end
+
+ context 'when an entry does not have a source key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /source key/)
+ end
+ end
+
+ context 'when an entry does not have a public key' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /public key/)
+ end
+ end
+
+ context 'when an entry source is not a valid regex' do
+ it 'raises an error' do
+ expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }.
+ to raise_error(Gitlab::RouteMap::FormatError, /regular expression/)
+ end
+ end
+
+ context 'when all is good' do
+ it 'returns a route map' do
+ route_map = described_class.new(YAML.dump([{ 'source' => 'index.haml', 'public' => 'index.html' }, { 'source' => '/(.*)\.md/', 'public' => '\1.html' }]))
+
+ expect(route_map.public_path_for_source_path('index.haml')).to eq('index.html')
+ expect(route_map.public_path_for_source_path('foo.md')).to eq('foo.html')
+ end
+ end
+ end
+
+ describe '#public_path_for_source_path' do
+ subject do
+ described_class.new(<<-'MAP'.strip_heredoc)
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+
+ # Blogposts
+ - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb
+ public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/
+
+ # HTML files
+ - source: /source/(.+?\.html).*/ # source/index.html.haml
+ public: '\1' # index.html
+
+ # Other files
+ - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png
+ public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png
+ MAP
+ end
+
+ it 'returns the public path for a provided source path' do
+ expect(subject.public_path_for_source_path('data/team.yml')).to eq('team/')
+
+ expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/')
+
+ expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html')
+
+ expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png')
+
+ expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/serialize/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb
index 7ea74da5252..b810c68ea03 100644
--- a/spec/lib/gitlab/serialize/ci/variables_spec.rb
+++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Serialize::Ci::Variables do
+describe Gitlab::Serializer::Ci::Variables do
subject do
described_class.load(described_class.dump(object))
end
diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb
new file mode 100644
index 00000000000..519eb1b274f
--- /dev/null
+++ b/spec/lib/gitlab/serializer/pagination_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::Serializer::Pagination do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:headers) { spy('headers') }
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(params)
+
+ allow(response).to receive(:headers)
+ .and_return(headers)
+ end
+
+ let(:pagination) { described_class.new(request, response) }
+
+ describe '#paginate' do
+ subject { pagination.paginate(resource) }
+
+ let(:resource) { User.all }
+ let(:params) { { page: 1, per_page: 2 } }
+
+ context 'when a multiple resources are present in relation' do
+ before { create_list(:user, 3) }
+
+ it 'correctly paginates the resource' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(headers).to receive(:[]=).with('X-Total', '3')
+ expect(headers).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(headers).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when an invalid resource is about to be paginated' do
+ let(:resource) { create(:user) }
+
+ it 'raises error' do
+ expect { subject }.to raise_error(
+ described_class::InvalidResourceError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 45cec65a284..1335a2b8f35 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -4,16 +4,14 @@ describe Gitlab::Template::IssueTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
- let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ path: 'issue_templates' })
end
describe '.all' do
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index ae51b79be22..320b870309a 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -4,16 +4,14 @@ describe Gitlab::Template::MergeRequestTemplate do
subject { described_class }
let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
- let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
- let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
-
- before do
- project.add_user(user, Gitlab::Access::MASTER)
- project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
- project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
- project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+
+ let(:project) do
+ create(:project,
+ :repository,
+ create_template: {
+ user: user,
+ access: Gitlab::Access::MASTER,
+ path: 'merge_request_templates' })
end
describe '.all' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 7dd4d76d1a3..a32c6131030 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do
out = {
subprotocols: ['foo'],
url: 'wss://example.com/terminal.ws',
- headers: { 'Authorization' => ['Token x'] }
+ headers: { 'Authorization' => ['Token x'] },
+ max_session_time: 600
}
out[:ca_pem] = ca_pem if ca_pem
out
@@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do
'Terminal' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
- 'Header' => { 'Authorization' => ['Token x'] }
+ 'Header' => { 'Authorization' => ['Token x'] },
+ 'MaxSessionTime' => 600
}
}
out['Terminal']['CAPem'] = ca_pem if ca_pem
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 2f4a33a1868..30f8fdf91b2 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -247,7 +247,7 @@ describe Ability, lib: true do
end
describe '.project_disabled_features_rules' do
- let(:project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED) }
+ let(:project) { create(:empty_project, :wiki_disabled) }
subject { described_class.allowed(project.owner, project) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 47cd5075a7d..4080092405d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -221,6 +221,47 @@ describe Ci::Build, :models do
end
end
+ describe '#coverage_regex' do
+ subject { build.coverage_regex }
+
+ context 'when project has build_coverage_regex set' do
+ let(:project_regex) { '\(\d+\.\d+\) covered' }
+
+ before do
+ project.build_coverage_regex = project_regex
+ end
+
+ context 'and coverage_regex attribute is not set' do
+ it { is_expected.to eq(project_regex) }
+ end
+
+ context 'but coverage_regex attribute is also set' do
+ let(:build_regex) { 'Code coverage: \d+\.\d+' }
+
+ before do
+ build.coverage_regex = build_regex
+ end
+
+ it { is_expected.to eq(build_regex) }
+ end
+ end
+
+ context 'when neither project nor build has coverage regex set' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#update_coverage' do
+ context "regarding coverage_regex's value," do
+ it "saves the correct extracted coverage value" do
+ build.coverage_regex = '\(\d+.\d+\%\) covered'
+ allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
+ expect(build).to receive(:update_attributes).with(coverage: 98.29) { true }
+ expect(build.update_coverage).to be true
+ end
+ end
+ end
+
describe 'deployment' do
describe '#last_deployment' do
subject { build.last_deployment }
@@ -443,11 +484,11 @@ describe Ci::Build, :models do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
subject { build.erased? }
- context 'build has not been erased' do
+ context 'job has not been erased' do
it { is_expected.to be_falsey }
end
- context 'build has been erased' do
+ context 'job has been erased' do
before do
build.erase
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 30443534cca..e008ec28fa4 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -14,12 +14,14 @@ describe Group, 'Routable' do
describe 'Callbacks' do
it 'creates route record on create' do
expect(group.route.path).to eq(group.path)
+ expect(group.route.name).to eq(group.name)
end
it 'updates route record on path change' do
- group.update_attributes(path: 'wow')
+ group.update_attributes(path: 'wow', name: 'much')
expect(group.route.path).to eq('wow')
+ expect(group.route.name).to eq('much')
end
it 'ensure route path uniqueness across different objects' do
@@ -78,4 +80,34 @@ describe Group, 'Routable' do
it { is_expected.to eq([nested_group]) }
end
+
+ describe '#full_path' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_path).to eq(group.path) }
+ it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
+ end
+
+ describe '#full_name' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+
+ it { expect(group.full_name).to eq(group.name) }
+ it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+ end
+end
+
+describe Project, 'Routable' do
+ describe '#full_path' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.full_path).to eq "#{project.namespace.path}/#{project.path}" }
+ end
+
+ describe '#full_name' do
+ let(:project) { build_stubbed(:empty_project) }
+
+ it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" }
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 3b7cc7d9e2e..9053485939e 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- deploy_master
- end
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
expect(subject[:code].median).to be_nil
end
@@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do
context "when a regular merge request (that doesn't close the issue) is created" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project)
- create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:code].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 5c73edbbc53..fc7d18bd40e 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
- 5.times do
- regular_label = create(:label)
- issue = create(:issue, project: project)
- issue.update(label_ids: [regular_label.id])
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
- create_merge_request_closing_issue(issue)
- merge_merge_requests_closing_issue(issue)
- end
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
expect(subject[:issue].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 591bbdddf55..b9fe492fe2c 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -21,7 +21,13 @@ describe 'CycleAnalytics#production', feature: true do
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
- sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ sha = context.project.repository.commit_file(
+ context.user,
+ context.random_git_name,
+ 'content',
+ message: 'commit message',
+ branch_name: 'master',
+ update: false)
context.project.repository.commit(sha)
context.deploy_master
@@ -29,11 +35,9 @@ describe 'CycleAnalytics#production', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
expect(subject[:production].median).to be_nil
end
@@ -41,12 +45,10 @@ describe 'CycleAnalytics#production', feature: true do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
expect(subject[:production].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 33d2c0a7416..febb18c9884 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do
context "when a regular merge request (that doesn't close the issue) is created and merged" do
it "returns nil" do
- 5.times do
- MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
- end
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
expect(subject[:review].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 00693d67475..9a024d533a1 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -29,10 +29,10 @@ describe 'CycleAnalytics#staging', feature: true do
sha = context.project.repository.commit_file(
context.user,
context.random_git_name,
- "content",
- "commit message",
- 'master',
- false)
+ 'content',
+ message: 'commit message',
+ branch_name: 'master',
+ update: false)
context.project.repository.commit(sha)
context.deploy_master
@@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
- 5.times do
- merge_request = create(:merge_request)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
- end
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
expect(subject[:staging].median).to be_nil
end
@@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
- end
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
expect(subject[:staging].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index f857ea6cbec..c2ba012a0e6 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.succeed!
+ pipeline.run!
+ pipeline.succeed!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
@@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is not for a merge request" do
it "returns nil" do
- 5.times do
- pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
- pipeline.run!
- pipeline.succeed!
- end
+ pipeline.run!
+ pipeline.succeed!
expect(subject[:test].median).to be_nil
end
@@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is dropped (failed)" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.drop!
+ pipeline.run!
+ pipeline.drop!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
@@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do
context "when the pipeline is cancelled" do
it "returns nil" do
- 5.times do
- issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
- pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
- pipeline.run!
- pipeline.cancel!
+ pipeline.run!
+ pipeline.cancel!
- merge_merge_requests_closing_issue(issue)
- end
+ merge_merge_requests_closing_issue(issue)
expect(subject[:test].median).to be_nil
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index fc4435a2f64..080ff2f3f43 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -77,8 +77,8 @@ describe Deployment, models: true do
end
end
- describe '#stoppable?' do
- subject { deployment.stoppable? }
+ describe '#stop_action?' do
+ subject { deployment.stop_action? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index b2e06541e66..960f29f3805 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -7,8 +7,6 @@ describe Environment, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
-
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
@@ -22,6 +20,20 @@ describe Environment, models: true do
it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
+ describe '.order_by_last_deployed_at' do
+ let(:project) { create(:project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let!(:environment3) { create(:environment, project: project) }
+ let!(:deployment1) { create(:deployment, environment: environment1) }
+ let!(:deployment2) { create(:deployment, environment: environment2) }
+ let!(:deployment3) { create(:deployment, environment: environment1) }
+
+ it 'returns the environments in order of having been last deployed' do
+ expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1])
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -112,8 +124,8 @@ describe Environment, models: true do
end
end
- describe '#stoppable?' do
- subject { environment.stoppable? }
+ describe '#stop_action?' do
+ subject { environment.stop_action? }
context 'when no other actions' do
it { is_expected.to be_falsey }
@@ -142,17 +154,39 @@ describe Environment, models: true do
end
end
- describe '#stop!' do
+ describe '#stop_with_action!' do
let(:user) { create(:user) }
- subject { environment.stop!(user) }
+ subject { environment.stop_with_action!(user) }
before do
- expect(environment).to receive(:stoppable?).and_call_original
+ expect(environment).to receive(:available?).and_call_original
end
context 'when no other actions' do
- it { is_expected.to be_nil }
+ context 'environment is available' do
+ before do
+ environment.update(state: :available)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
+
+ context 'environment is already stopped' do
+ before do
+ environment.update(state: :stopped)
+ end
+
+ it do
+ subject
+
+ expect(environment).to be_stopped
+ end
+ end
end
context 'when matching action is defined' do
@@ -288,6 +322,11 @@ describe Environment, models: true do
"1-foo" => "env-1-foo" + SUFFIX,
"1/foo" => "env-1-foo" + SUFFIX,
"foo-" => "foo" + SUFFIX,
+ "foo--bar" => "foo-bar" + SUFFIX,
+ "foo**bar" => "foo-bar" + SUFFIX,
+ "*-foo" => "env-foo" + SUFFIX,
+ "staging-12345678-" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
@@ -296,4 +335,33 @@ describe Environment, models: true do
end
end
end
+
+ describe '#external_url_for' do
+ let(:source_path) { 'source/file.html' }
+ let(:sha) { RepoHelpers.sample_commit.id }
+
+ before do
+ environment.external_url = 'http://example.com'
+ end
+
+ context 'when the public path is not known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(environment.external_url_for(source_path, sha)).to be_nil
+ end
+ end
+
+ context 'when the public path is known' do
+ before do
+ allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html')
+ end
+
+ it 'returns the full external URL' do
+ expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html')
+ end
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 349474bb656..8c90a538f57 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -19,7 +19,7 @@ describe Event, models: true do
let(:project) { create(:empty_project) }
it 'calls the reset_project_activity method' do
- expect_any_instance_of(Event).to receive(:reset_project_activity)
+ expect_any_instance_of(described_class).to receive(:reset_project_activity)
create_event(project, project.owner)
end
@@ -43,33 +43,33 @@ describe Event, models: true do
describe '#membership_changed?' do
context "created" do
- subject { build(:event, action: Event::CREATED).membership_changed? }
+ subject { build(:event, :created).membership_changed? }
it { is_expected.to be_falsey }
end
context "updated" do
- subject { build(:event, action: Event::UPDATED).membership_changed? }
+ subject { build(:event, :updated).membership_changed? }
it { is_expected.to be_falsey }
end
context "expired" do
- subject { build(:event, action: Event::EXPIRED).membership_changed? }
+ subject { build(:event, :expired).membership_changed? }
it { is_expected.to be_truthy }
end
context "left" do
- subject { build(:event, action: Event::LEFT).membership_changed? }
+ subject { build(:event, :left).membership_changed? }
it { is_expected.to be_truthy }
end
context "joined" do
- subject { build(:event, action: Event::JOINED).membership_changed? }
+ subject { build(:event, :joined).membership_changed? }
it { is_expected.to be_truthy }
end
end
describe '#note?' do
- subject { Event.new(project: target.project, target: target) }
+ subject { described_class.new(project: target.project, target: target) }
context 'issue note event' do
let(:target) { create(:note_on_issue) }
@@ -97,7 +97,7 @@ describe Event, models: true do
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) }
+ let(:event) { described_class.new(project: project, target: target, author_id: author.id) }
before do
project.team << [member, :developer]
@@ -221,13 +221,13 @@ describe Event, models: true do
let!(:event2) { create(:closed_issue_event) }
describe 'without an explicit limit' do
- subject { Event.limit_recent }
+ subject { described_class.limit_recent }
it { is_expected.to eq([event2, event1]) }
end
describe 'with an explicit limit' do
- subject { Event.limit_recent(1) }
+ subject { described_class.limit_recent(1) }
it { is_expected.to eq([event2]) }
end
@@ -294,9 +294,9 @@ describe Event, models: true do
}
}
- Event.create({
+ described_class.create({
project: project,
- action: Event::PUSHED,
+ action: described_class::PUSHED,
data: data,
author_id: user.id
}.merge!(attrs))
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9ca50555191..a4e6eb4e3a6 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -300,4 +300,17 @@ describe Group, models: true do
expect(group.members_with_parents).to include(master)
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ master = create(:user)
+ developer = create(:user)
+
+ group.add_user(master, GroupMember::MASTER)
+ group.add_user(developer, GroupMember::DEVELOPER)
+
+ expect(group.user_ids_for_project_authorizations).
+ to include(master.id, developer.id)
+ end
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index 582b54c0712..c60bd7af958 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -37,8 +37,6 @@ describe Guest, lib: true do
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
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 40c0a75c364..bba9058f394 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -23,21 +23,74 @@ describe Issue, models: true do
end
describe '#to_reference' do
- let(:project) { build(:empty_project, name: 'sample-project') }
- let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
+ let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+
+ context 'when nil argument' do
+ it 'returns issue id' do
+ expect(issue.to_reference).to eq "#1"
+ end
+ end
+
+ context 'when full is true' do
+ it 'returns complete path to the issue' do
+ expect(issue.to_reference(full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
+ expect(issue.to_reference(group, full: true)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
- it 'returns a String reference to the object' do
- expect(issue.to_reference).to eq "#1"
+ context 'when same project argument' do
+ it 'returns issue id' do
+ expect(issue.to_reference(project)).to eq("#1")
+ end
end
- it 'returns a String reference with the full path' do
- expect(issue.to_reference(full: true)).to eq(project.path_with_namespace + '#1')
+ context 'when cross namespace project argument' do
+ let(:another_namespace_project) { create(:empty_project, name: 'another-project') }
+
+ it 'returns complete path to the issue' do
+ expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
+ end
end
it 'supports a cross-project reference' do
another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
expect(issue.to_reference(another_project)).to eq "sample-project#1"
end
+
+ context 'when same namespace / cross-project argument' do
+ let(:another_project) { create(:empty_project, namespace: namespace) }
+
+ it 'returns path to the issue with the project name' do
+ expect(issue.to_reference(another_project)).to eq 'sample-project#1'
+ end
+ end
+
+ context 'when different namespace / cross-project argument' do
+ let(:another_namespace) { create(:namespace, path: 'another-namespace') }
+ let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) }
+
+ it 'returns full path to the issue' do
+ expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
+
+ context 'when argument is a namespace' do
+ context 'with same project path' do
+ it 'returns path to the issue with the project name' do
+ expect(issue.to_reference(namespace)).to eq 'sample-project#1'
+ end
+ end
+
+ context 'with different project path' do
+ it 'returns full path to the issue' do
+ expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
+ end
+ end
+ end
end
describe '#is_being_reassigned?' do
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index 9e1a52011c3..e6ca4853873 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -19,13 +19,6 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
- context 'when list_type is set to backlog' do
- subject { described_class.new(list_type: :backlog) }
-
- it { is_expected.not_to validate_presence_of(:label) }
- it { is_expected.not_to validate_presence_of(:position) }
- end
-
context 'when list_type is set to done' do
subject { described_class.new(list_type: :done) }
@@ -41,12 +34,6 @@ describe List do
expect(subject.destroy).to be_truthy
end
- it 'can not be destroyed when list_type is set to backlog' do
- subject = create(:backlog_list)
-
- expect(subject.destroy).to be_falsey
- end
-
it 'can not be destroyed when when list_type is set to done' do
subject = create(:done_list)
@@ -55,19 +42,13 @@ describe List do
end
describe '#destroyable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_destroyable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_destroyable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_destroyable
@@ -75,19 +56,13 @@ describe List do
end
describe '#movable?' do
- it 'retruns true when list_type is set to label' do
+ it 'returns true when list_type is set to label' do
subject.list_type = :label
expect(subject).to be_movable
end
- it 'retruns false when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject).not_to be_movable
- end
-
- it 'retruns false when list_type is set to done' do
+ it 'returns false when list_type is set to done' do
subject.list_type = :done
expect(subject).not_to be_movable
@@ -102,12 +77,6 @@ describe List do
expect(subject.title).to eq 'Development'
end
- it 'returns Backlog when list_type is set to backlog' do
- subject.list_type = :backlog
-
- expect(subject.title).to eq 'Backlog'
- end
-
it 'returns Done when list_type is set to done' do
subject.list_type = :done
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 16e2144d6a1..c720cc9f2c2 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -129,6 +129,14 @@ describe Member, models: true do
it { expect(described_class.request).not_to include @accepted_request_member }
end
+ describe '.non_request' do
+ it { expect(described_class.non_request).to include @master }
+ it { expect(described_class.non_request).to include @invited_member }
+ it { expect(described_class.non_request).to include @accepted_invite_member }
+ it { expect(described_class.non_request).not_to include @requested_member }
+ it { expect(described_class.non_request).to include @accepted_request_member }
+ end
+
describe '.developers' do
subject { described_class.developers.to_a }
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 90d14c2c0b9..e4be0aba7a6 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -117,7 +117,7 @@ describe ProjectMember, models: true do
users = create_list(:user, 2)
described_class.add_users_to_projects(
- [projects.first.id, projects.second],
+ [projects.first.id, projects.second.id],
[users.first.id, users.second],
described_class::MASTER)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 32ed1e96749..a01741a9971 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -97,7 +97,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
@@ -106,7 +106,7 @@ describe MergeRequest, models: true do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
@@ -300,7 +300,7 @@ describe MergeRequest, models: true do
allow(subject.project).to receive(:default_branch).
and_return(subject.target_branch)
- expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
+ expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
end
end
@@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do
end
end
- describe "#environments" do
+ describe "#environments_for" do
let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
+ before do
+ merge_request.source_project.add_master(user)
+ merge_request.target_project.add_master(user)
+ end
+
context 'with multiple environments' do
let(:environments) { create_list(:environment, 3, project: project) }
@@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(environments.first)
+ expect(merge_request.environments_for(user)).to contain_exactly(environments.first)
end
end
@@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment)
end
context 'with environments on target project' do
@@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do
end
it 'selects deployed environments' do
- expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment)
end
end
end
@@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do
end
it 'returns an empty array' do
- expect(merge_request.environments).to be_empty
+ expect(merge_request.environments_for(user)).to be_empty
end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4e96f19eb6f..35d932f1c64 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -3,21 +3,32 @@ require 'spec_helper'
describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
- it { is_expected.to have_many :projects }
- it { is_expected.to have_many :project_statistics }
- it { is_expected.to belong_to :parent }
- it { is_expected.to have_many :children }
+ describe 'associations' do
+ it { is_expected.to have_many :projects }
+ it { is_expected.to have_many :project_statistics }
+ it { is_expected.to belong_to :parent }
+ it { is_expected.to have_many :children }
+ end
- it { is_expected.to validate_presence_of(:name) }
- it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
- it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ it { is_expected.to validate_length_of(:description).is_at_most(255) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_length_of(:path).is_at_most(255) }
+ it { is_expected.to validate_presence_of(:owner) }
- it { is_expected.to validate_length_of(:description).is_at_most(255) }
+ it 'does not allow too deep nesting' do
+ ancestors = (1..21).to_a
+ nested = build(:namespace, parent: namespace)
- it { is_expected.to validate_presence_of(:path) }
- it { is_expected.to validate_length_of(:path).is_at_most(255) }
+ allow(nested).to receive(:ancestors).and_return(ancestors)
- it { is_expected.to validate_presence_of(:owner) }
+ expect(nested).not_to be_valid
+ expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting')
+ end
+ end
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
@@ -175,22 +186,6 @@ describe Namespace, models: true do
end
end
- describe '#full_path' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
-
- it { expect(group.full_path).to eq(group.path) }
- it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
- end
-
- describe '#full_name' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
-
- it { expect(group.full_name).to eq(group.name) }
- it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
- end
-
describe '#ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
@@ -218,4 +213,11 @@ describe Namespace, models: true do
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
+
+ describe '#user_ids_for_project_authorizations' do
+ it 'returns the user IDs for which to refresh authorizations' do
+ expect(namespace.user_ids_for_project_authorizations).
+ to eq([namespace.owner_id])
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
new file mode 100644
index 00000000000..e6a4583a8fb
--- /dev/null
+++ b/spec/models/pages_domain_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe PagesDomain, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe :validate_domain do
+ subject { build(:pages_domain, domain: domain) }
+
+ context 'is unique' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to validate_uniqueness_of(:domain) }
+ end
+
+ context 'valid domain' do
+ let(:domain) { 'my.domain.com' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'valid hexadecimal-looking domain' do
+ let(:domain) { '0x12345.com'}
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'no domain' do
+ let(:domain) { nil }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'invalid domain' do
+ let(:domain) { '0123123' }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'domain from .example.com' do
+ let(:domain) { 'my.domain.com' }
+
+ before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe 'validate certificate' do
+ subject { domain }
+
+ context 'when only certificate is specified' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'when only key is specified' do
+ let(:domain) { build(:pages_domain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'for not matching key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe :url do
+ subject { domain.url }
+
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain) }
+
+ it { is_expected.to eq('http://my.domain.com') }
+ end
+
+ context 'with a certificate' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to eq('https://my.domain.com') }
+ end
+ end
+
+ describe :has_matching_key? do
+ subject { domain.has_matching_key? }
+
+ context 'for matching key' do
+ let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for invalid key' do
+ let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe :has_intermediates? do
+ subject { domain.has_intermediates? }
+
+ context 'for self signed' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for missing certificate chain' do
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for trusted certificate chain' do
+ # We only validate that we can to rebuild the trust chain, for certificates
+ # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store.
+ # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine
+
+ let(:domain) { build(:pages_domain, :with_trusted_chain) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :expired? do
+ subject { domain.expired? }
+
+ context 'for valid' do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'for expired' do
+ let(:domain) { build(:pages_domain, :with_expired_certificate) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe :subject do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.subject }
+
+ it { is_expected.to eq('/CN=test-certificate') }
+ end
+
+ describe :certificate_text do
+ let(:domain) { build(:pages_domain, :with_certificate) }
+
+ subject { domain.certificate_text }
+
+ # We test only existence of output, since the output is long
+ it { is_expected.not_to be_empty }
+ end
+end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 8589f1eb712..09a4448d387 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -57,7 +57,6 @@ describe ProjectFeature do
context 'when feature is enabled for everyone' do
it "returns true" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.feature_available?(:issues, user)).to eq(true)
end
end
@@ -104,7 +103,6 @@ describe ProjectFeature do
it "returns true when feature is enabled for everyone" do
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED)
expect(project.public_send("#{feature}_enabled?")).to eq(true)
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 2f6b159d76e..4bca0229e7a 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -135,7 +135,7 @@ describe JiraService, models: true do
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" } }
+ status: { resolved: true }
}
)
).once
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 4f3cd14e941..9052479d35e 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -181,11 +181,23 @@ describe KubernetesService, models: true, caching: true do
let(:pod) { kube_pod(app: environment.slug) }
let(:terminals) { kube_terminals(service, pod) }
- it 'returns terminals' do
- stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ])
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]
+ )
+ end
+ it 'returns terminals' do
is_expected.to eq(terminals + terminals)
end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
end
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
index c879edddfdd..98f3d420c8a 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -113,10 +113,7 @@ describe MattermostSlashCommandsService, :models do
end
it 'shows error messages' do
- teams, message = subject
-
- expect(teams).to be_empty
- expect(message).to eq('Failed to get team list.')
+ expect(subject).to eq([[], "Failed to get team list."])
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 646a1311462..35f3dd00870 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -60,6 +60,7 @@ describe Project, models: true do
it { is_expected.to have_many(:runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:pages_domains) }
it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
it { is_expected.to have_many(:environments).dependent(:destroy) }
@@ -274,17 +275,11 @@ describe Project, models: true do
it { is_expected.to delegate_method(:add_master).to(:team) }
end
- describe '#name_with_namespace' do
- let(:project) { build_stubbed(:empty_project) }
-
- it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
- it { expect(project.human_name).to eq project.name_with_namespace }
- end
-
describe '#to_reference' do
- let(:owner) { create(:user, name: 'Gitlab') }
+ let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
- let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) }
+ let(:project) { create(:empty_project, path: 'sample-project', namespace: namespace) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) }
context 'when nil argument' do
it 'returns nil' do
@@ -292,6 +287,14 @@ describe Project, models: true do
end
end
+ context 'when full is true' do
+ it 'returns complete path to the project' do
+ expect(project.to_reference(full: true)).to eq 'sample-namespace/sample-project'
+ expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project'
+ expect(project.to_reference(group, full: true)).to eq 'sample-namespace/sample-project'
+ end
+ end
+
context 'when same project argument' do
it 'returns nil' do
expect(project.to_reference(project)).to be_nil
@@ -309,10 +312,33 @@ describe Project, models: true do
context 'when same namespace / cross-project argument' do
let(:another_project) { create(:empty_project, namespace: namespace) }
- it 'returns complete path to the project' do
+ it 'returns path to the project' do
expect(project.to_reference(another_project)).to eq 'sample-project'
end
end
+
+ context 'when different namespace / cross-project argument' do
+ let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
+ let(:another_project) { create(:empty_project, path: 'another-project', namespace: another_namespace) }
+
+ it 'returns full path to the project' do
+ expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project'
+ end
+ end
+
+ context 'when argument is a namespace' do
+ context 'with same project path' do
+ it 'returns path to the project' do
+ expect(project.to_reference(namespace)).to eq 'sample-project'
+ end
+ end
+
+ context 'with different project path' do
+ it 'returns full path to the project' do
+ expect(project.to_reference(group)).to eq 'sample-namespace/sample-project'
+ end
+ end
+ end
end
describe '#to_human_reference' do
@@ -600,7 +626,7 @@ describe Project, models: true do
end
describe '#has_wiki?' do
- let(:no_wiki_project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
+ let(:no_wiki_project) { create(:empty_project, :wiki_disabled, has_external_wiki: false) }
let(:wiki_enabled_project) { create(:empty_project) }
let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) }
@@ -1035,6 +1061,22 @@ describe Project, models: true do
end
end
+ describe '#pages_deployed?' do
+ let(:project) { create :empty_project }
+
+ subject { project.pages_deployed? }
+
+ context 'if public folder does exist' do
+ before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "if public folder doesn't exist" do
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '.search' do
let(:project) { create(:empty_project, description: 'kitten mittens') }
@@ -1667,141 +1709,172 @@ describe Project, models: true do
end
end
- describe '#environments_for' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
-
- context 'tagged deployment' do
- before do
- create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
- end
+ describe '#deployment_variables' do
+ context 'when project has no deployment service' do
+ let(:project) { create(:empty_project) }
- it 'returns environment when with_tags is set' do
- expect(project.environments_for('master', commit: project.commit, with_tags: true))
- .to contain_exactly(environment)
+ it 'returns an empty array' do
+ expect(project.deployment_variables).to eq []
end
+ end
- it 'does not return environment when no with_tags is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to be_empty
- end
+ context 'when project has a deployment service' do
+ let(:project) { create(:kubernetes_project) }
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
+ )
end
end
+ end
- context 'branch deployment' do
- before do
- create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
- end
+ describe '#update_project_statistics' do
+ let(:project) { create(:empty_project) }
- it 'returns environment when ref is set' do
- expect(project.environments_for('master', commit: project.commit))
- .to contain_exactly(environment)
- end
+ it "is called after creation" do
+ expect(project.statistics).to be_a ProjectStatistics
+ expect(project.statistics).to be_persisted
+ end
- it 'does not environment when ref is different' do
- expect(project.environments_for('feature', commit: project.commit))
- .to be_empty
- end
+ it "copies the namespace_id" do
+ expect(project.statistics.namespace_id).to eq project.namespace_id
+ end
- it 'does not return environment when commit is not part of deployment' do
- expect(project.environments_for('master', commit: project.commit('feature')))
- .to be_empty
- end
+ it "updates the namespace_id when changed" do
+ namespace = create(:namespace)
+ project.update(namespace: namespace)
- it 'returns environment when commit constraint is not set' do
- expect(project.environments_for('master'))
- .to contain_exactly(environment)
- end
+ expect(project.statistics.namespace_id).to eq namespace.id
end
end
- describe '#environments_recently_updated_on_branch' do
- let(:project) { create(:project, :repository) }
- let(:environment) { create(:environment, project: project) }
+ describe 'inside_path' do
+ let!(:project1) { create(:empty_project) }
+ let!(:project2) { create(:empty_project) }
+ let!(:path) { project1.namespace.path }
- context 'when last deployment to environment is the most recent one' do
- before do
- create(:deployment, environment: environment, ref: 'feature')
- end
+ it { expect(Project.inside_path(path)).to eq([project1]) }
+ end
- it 'finds recently updated environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment)
- end
+ describe '#route_map_for' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ <<-MAP.strip_heredoc
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
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
+ before do
+ project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
- it 'does not find environment' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to be_empty
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ context 'when the route map is valid' do
+ it 'returns a route map' do
+ map = project.route_map_for(project.commit.sha)
+ expect(map).to be_a_kind_of(Gitlab::RouteMap)
+ end
end
- end
- context 'when there are two environments that deploy to the same branch' do
- let(:second_environment) { create(:environment, project: project) }
+ context 'when the route map is invalid' do
+ let(:route_map) { 'INVALID' }
- before do
- create(:deployment, environment: environment, ref: 'feature')
- create(:deployment, environment: second_environment, ref: 'feature')
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.sha)).to be_nil
+ end
end
+ end
- it 'finds both environments' do
- expect(project.environments_recently_updated_on_branch('feature'))
- .to contain_exactly(environment, second_environment)
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(project.route_map_for(project.commit.parent.sha)).to be_nil
end
end
end
- describe '#deployment_variables' do
- context 'when project has no deployment service' do
- let(:project) { create(:empty_project) }
+ describe '#public_path_for_source_path' do
+ let(:project) { create(:project) }
+ let(:route_map) do
+ Gitlab::RouteMap.new(<<-MAP.strip_heredoc)
+ - source: /source/(.*)/
+ public: '\\1'
+ MAP
+ end
+ let(:sha) { project.commit.id }
- it 'returns an empty array' do
- expect(project.deployment_variables).to eq []
+ context 'when there is a route map' do
+ before do
+ allow(project).to receive(:route_map_for).with(sha).and_return(route_map)
+ end
+
+ context 'when the source path is mapped' do
+ it 'returns the public path' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html')
+ end
+ end
+
+ context 'when the source path is not mapped' do
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('file.html', sha)).to be_nil
+ end
end
end
- context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ context 'when there is no route map' do
+ before do
+ allow(project).to receive(:route_map_for).with(sha).and_return(nil)
+ end
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
+ it 'returns nil' do
+ expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil
end
end
end
- describe '#update_project_statistics' do
+ describe '#parent' do
let(:project) { create(:empty_project) }
- it "is called after creation" do
- expect(project.statistics).to be_a ProjectStatistics
- expect(project.statistics).to be_persisted
- end
+ it { expect(project.parent).to eq(project.namespace) }
+ end
- it "copies the namespace_id" do
- expect(project.statistics.namespace_id).to eq project.namespace_id
- end
+ describe '#parent_changed?' do
+ let(:project) { create(:empty_project) }
- it "updates the namespace_id when changed" do
- namespace = create(:namespace)
- project.update(namespace: namespace)
+ before { project.namespace_id = 7 }
- expect(project.statistics.namespace_id).to eq namespace.id
- end
+ it { expect(project.parent_changed?).to be_truthy }
end
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
+
+ describe '#pages_url' do
+ let(:group) { create :group, name: group_name }
+ let(:project) { create :empty_project, namespace: group, name: project_name }
+ let(:domain) { 'Example.com' }
+
+ subject { project.pages_url }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
+ end
+
+ context 'group page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'group.example.com' }
+
+ it { is_expected.to eq("http://group.example.com") }
+ end
+
+ context 'project page' do
+ let(:group_name) { 'Group' }
+ let(:project_name) { 'Project' }
+
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 829b69093c9..9bfa6409607 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -15,7 +15,12 @@ describe Repository, models: true do
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
+
repository.commit(merge_commit_id)
end
@@ -289,17 +294,39 @@ describe Repository, models: true do
describe "#commit_dir" do
it "commits a change that creates a new directory" do
expect do
- repository.commit_dir(user, 'newdir', 'Create newdir', 'master')
+ repository.commit_dir(user, 'newdir',
+ message: 'Create newdir', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
newdir = repository.tree('master', 'newdir')
expect(newdir.path).to eq('newdir')
end
+ context "when committing to another project" do
+ let(:forked_project) { create(:project) }
+
+ it "creates a fork and commit to the forked project" do
+ expect do
+ repository.commit_dir(user, 'newdir',
+ message: 'Create newdir', branch_name: 'patch',
+ start_branch_name: 'master', start_project: forked_project)
+ end.to change { repository.commits('master').count }.by(0)
+
+ expect(repository.branch_exists?('patch')).to be_truthy
+ expect(forked_project.repository.branch_exists?('patch')).to be_falsy
+
+ newdir = repository.tree('patch', 'newdir')
+ expect(newdir.path).to eq('newdir')
+ end
+ end
+
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name)
+ repository.commit_dir(user, 'newdir',
+ message: 'Add newdir',
+ branch_name: 'master',
+ author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -314,8 +341,9 @@ describe Repository, models: true do
it 'commits change to a file successfully' do
expect do
repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- 'Updates file content',
- 'master', true)
+ message: 'Updates file content',
+ branch_name: 'master',
+ update: true)
end.to change { repository.commits('master').count }.by(1)
blob = repository.blob_at('master', 'CHANGELOG')
@@ -326,8 +354,12 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
expect do
- repository.commit_file(user, "README", 'README!', 'Add README',
- 'master', true, author_email: author_email, author_name: author_name)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README',
+ branch_name: 'master',
+ update: true,
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -342,7 +374,7 @@ describe Repository, models: true do
it 'updates filename successfully' do
expect do
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
- branch: 'master',
+ branch_name: 'master',
previous_path: 'LICENSE',
message: 'Changes filename')
end.to change { repository.commits('master').count }.by(1)
@@ -355,15 +387,16 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.update_file(user, 'README', "Updated README!",
- branch: 'master',
- previous_path: 'README',
- message: 'Update README',
- author_email: author_email,
- author_name: author_name)
+ repository.update_file(user, 'README', 'Updated README!',
+ branch_name: 'master',
+ previous_path: 'README',
+ message: 'Update README',
+ author_email: author_email,
+ author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -376,10 +409,12 @@ describe Repository, models: true do
describe "#remove_file" do
it 'removes file successfully' do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.remove_file(user, "README", "Remove README", 'master')
+ repository.remove_file(user, 'README',
+ message: 'Remove README', branch_name: 'master')
end.to change { repository.commits('master').count }.by(1)
expect(repository.blob_at('master', 'README')).to be_nil
@@ -387,10 +422,13 @@ describe Repository, models: true do
context "when an author is specified" do
it "uses the given email/name to set the commit's author" do
- repository.commit_file(user, "README", 'README!', 'Add README', 'master', true)
+ repository.commit_file(user, 'README', 'README!',
+ message: 'Add README', branch_name: 'master', update: true)
expect do
- repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name)
+ repository.remove_file(user, 'README',
+ message: 'Remove README', branch_name: 'master',
+ author_email: author_email, author_name: author_name)
end.to change { repository.commits('master').count }.by(1)
last_commit = repository.commit
@@ -538,11 +576,14 @@ describe Repository, models: true do
describe "#license_blob", caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ repository.remove_file(
+ user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
end
it 'handles when HEAD points to non-existent ref' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(
+ user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
allow(repository).to receive(:file_on_head).
and_raise(Rugged::ReferenceError)
@@ -551,21 +592,27 @@ describe Repository, models: true do
end
it 'looks in the root_ref only' do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
- repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
+ repository.remove_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'markdown')
+ repository.commit_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ message: 'Add LICENSE', branch_name: 'markdown', update: false)
expect(repository.license_blob).to be_nil
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_blob.name).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
it "detects '#{filename}'" do
- repository.commit_file(user, filename, Licensee::License.new('mit').content, "Add #{filename}", 'master', false)
+ repository.commit_file(user, filename,
+ Licensee::License.new('mit').content,
+ message: "Add #{filename}", branch_name: 'master', update: false)
expect(repository.license_blob.name).to eq(filename)
end
@@ -574,7 +621,8 @@ describe Repository, models: true do
describe '#license_key', caching: true do
before do
- repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ repository.remove_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
@@ -588,13 +636,16 @@ describe Repository, models: true do
end
it 'detects license file with no recognizable open-source license content' do
- repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_key).to be_nil
end
it 'returns the license key' do
- repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE',
+ Licensee::License.new('mit').content,
+ message: 'Add LICENSE', branch_name: 'master', update: false)
expect(repository.license_key).to eq('mit')
end
@@ -707,7 +758,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.rm_branch(user, 'new_feature')
+ repository.rm_branch(user, 'feature')
end.to raise_error(GitHooksService::PreReceiveError)
end
@@ -728,36 +779,51 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
before do
- expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
- and_yield.and_return(true)
+ service = GitHooksService.new
+ expect(GitHooksService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute).
+ with(
+ user,
+ repository.path_to_repo,
+ old_rev,
+ new_rev,
+ 'refs/heads/feature').
+ and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
- expect(repository).to receive(:update_autocrlf_option)
+ service = GitOperationService.new(user, repository)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ expect(service).to receive(:update_autocrlf_option)
+
+ service.with_branch('feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
+
expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
end
end
end
context 'when the update adds more than one commit' do
- it 'runs without errors' do
- old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+ let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' }
+ it 'runs without errors' do
# old_rev is an ancestor of new_rev
expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
@@ -767,22 +833,28 @@ describe Repository, models: true do
branch = 'feature-ff-target'
repository.add_branch(user, branch, old_rev)
- expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+ expect do
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
+ end.not_to raise_error
end
end
context 'when the update would remove commits from the target branch' do
- it 'raises an exception' do
- branch = 'master'
- old_rev = repository.find_branch(branch).dereferenced_target.sha
+ let(:branch) { 'master' }
+ let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha }
+ it 'raises an exception' do
# The 'master' branch is NOT an ancestor of new_rev.
expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
expect do
- repository.update_branch_with_hooks(user, branch) { new_rev }
+ GitOperationService.new(user, repository).with_branch(branch) do
+ new_rev
+ end
end.to raise_error(Repository::CommitError)
end
end
@@ -792,7 +864,9 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ GitOperationService.new(user, repository).with_branch('feature') do
+ new_rev
+ end
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -800,7 +874,6 @@ describe Repository, models: true do
context 'when target branch is different from source branch' do
before do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
- allow(repository).to receive(:update_ref!)
end
it 'expires branch cache' do
@@ -809,7 +882,10 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
+ GitOperationService.new(user, repository).
+ with_branch('new-feature') do
+ new_rev
+ end
end
end
@@ -827,7 +903,9 @@ describe Repository, models: true do
expect(empty_repository).to receive(:expire_branches_cache)
empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!',
- 'Updates file content', 'master', false)
+ message: 'Updates file content',
+ branch_name: 'master',
+ update: false)
end
end
end
@@ -877,7 +955,7 @@ describe Repository, models: true do
end
it 'sets autocrlf to :input' do
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
expect(repository.raw_repository.autocrlf).to eq(:input)
end
@@ -892,7 +970,7 @@ describe Repository, models: true do
expect(repository.raw_repository).not_to receive(:autocrlf=).
with(:input)
- repository.update_autocrlf_option
+ GitOperationService.new(nil, repository).send(:update_autocrlf_option)
end
end
end
@@ -1009,8 +1087,11 @@ describe Repository, models: true do
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
- merge_commit_id = repository.merge(user, merge_request, commit_options)
- repository.commit(merge_commit_id)
+
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
@@ -1388,9 +1469,10 @@ describe Repository, models: true do
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
- expect(repository.rugged.tags).to receive(:delete).with('v1.1.0')
- repository.rm_tag('v1.1.0')
+ repository.rm_tag(create(:user), 'v1.1.0')
+
+ expect(repository.find_tag('v1.1.0')).to be_nil
end
end
@@ -1458,16 +1540,16 @@ describe Repository, models: true do
end
end
- describe '#update_ref!' do
+ describe '#update_ref' do
it 'can create a ref' do
- repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end
it 'raises CommitError when the ref update fails' do
expect do
- repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
end.to raise_error(Repository::CommitError)
end
end
@@ -1700,4 +1782,40 @@ describe Repository, models: true do
repository.refresh_method_caches(%i(readme license))
end
end
+
+ describe '#gitlab_ci_yml_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab-ci.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
+
+ describe '#route_map_for' do
+ before do
+ repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
+ end
+
+ context 'when there is a .gitlab/route-map.yml at the commit' do
+ it 'returns the content' do
+ expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
+ end
+ end
+
+ context 'when there is no .gitlab/route-map.yml at the commit' do
+ it 'returns nil' do
+ expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index dd2a5109abc..0b222022e62 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group, path: 'gitlab') }
+ let!(:group) { create(:group, path: 'gitlab', name: 'gitlab') }
let!(:route) { group.route }
describe 'relationships' do
@@ -15,17 +15,42 @@ describe Route, models: true do
end
describe '#rename_descendants' do
- let!(:nested_group) { create(:group, path: "test", parent: group) }
- let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
- let!(:similar_group) { create(:group, path: 'gitlab-org') }
+ let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
+ let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }
+ let!(:similar_group) { create(:group, path: 'gitlab-org', name: 'gitlab-org') }
- before { route.update_attributes(path: 'bar') }
+ context 'path update' do
+ context 'when route name is set' do
+ before { route.update_attributes(path: 'bar') }
- it "updates children routes with new path" do
- expect(described_class.exists?(path: 'bar')).to be_truthy
- expect(described_class.exists?(path: 'bar/test')).to be_truthy
- expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
- expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
+ it "updates children routes with new path" do
+ expect(described_class.exists?(path: 'bar')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test')).to be_truthy
+ expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
+ expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
+ end
+ end
+
+ context 'when route name is nil' do
+ before do
+ route.update_column(:name, nil)
+ end
+
+ it "does not fail" do
+ expect(route.update_attributes(path: 'bar')).to be_truthy
+ end
+ end
+ end
+
+ context 'name update' do
+ before { route.update_attributes(name: 'bar') }
+
+ it "updates children routes with new path" do
+ expect(described_class.exists?(name: 'bar')).to be_truthy
+ expect(described_class.exists?(name: 'bar / test')).to be_truthy
+ expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
+ expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
+ end
end
end
end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index f08935b6425..ebc694213b6 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -2,9 +2,37 @@ require 'rails_helper'
RSpec.describe Timelog, type: :model do
subject { build(:timelog) }
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) }
+
+ describe 'Issuable validation' do
+ it 'is invalid if issue_id and merge_request_id are missing' do
+ subject.attributes = { issue: nil, merge_request: nil }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is invalid if issue_id and merge_request_id are set' do
+ subject.attributes = { issue: issue, merge_request: merge_request }
+
+ expect(subject).to be_invalid
+ end
+
+ it 'is valid if only issue_id is set' do
+ subject.attributes = { issue: issue, merge_request: nil }
+
+ expect(subject).to be_valid
+ end
+
+ it 'is valid if only merge_request_id is set' do
+ subject.attributes = { merge_request: merge_request, issue: nil }
+
+ expect(subject).to be_valid
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 8017d1c3324..581305ad39f 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -109,7 +109,7 @@ describe Todo, models: true do
end
describe '#target_reference' do
- it 'returns the short commit id for commits' do
+ it 'returns commit full reference with short id' do
project = create(:project, :repository)
commit = project.commit
@@ -117,12 +117,12 @@ describe Todo, models: true do
subject.target_type = 'Commit'
subject.commit_id = commit.id
- expect(subject.target_reference).to eq commit.short_id
+ expect(subject.target_reference).to eq commit.reference_link_text(full: true)
end
- it 'returns reference for issuables' do
+ it 'returns full reference for issuables' do
subject.target = issue
- expect(subject.target_reference).to eq issue.to_reference
+ expect(subject.target_reference).to eq issue.to_reference(full: true)
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 6ca5ad747d1..7fd49c73b37 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -23,9 +23,9 @@ describe User, models: true do
it { is_expected.to have_many(:recent_events).class_name('Event') }
it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_issues).dependent(:destroy) }
+ it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
+ it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
@@ -141,6 +141,11 @@ describe User, models: true do
user = build(:user, email: "example@test.com")
expect(user).to be_invalid
end
+
+ it 'accepts example@test.com when added by another user' do
+ user = build(:user, email: "example@test.com", created_by_id: 1)
+ expect(user).to be_valid
+ end
end
context 'domain blacklist' do
@@ -159,6 +164,11 @@ describe User, models: true do
user = build(:user, email: 'info@example.com')
expect(user).not_to be_valid
end
+
+ it 'accepts info@example.com when added by another user' do
+ user = build(:user, email: 'info@example.com', created_by_id: 1)
+ expect(user).to be_valid
+ end
end
context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
@@ -1013,8 +1023,8 @@ describe User, models: true do
let!(:project2) { create(:empty_project, forked_from_project: project3) }
let!(:project3) { create(:empty_project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) }
- let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) }
+ let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
+ let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
project1.team << [subject, :master]
@@ -1058,7 +1068,7 @@ describe User, models: true do
let!(:push_data) do
Gitlab::DataBuilder::Push.build_sample(project2, subject)
end
- let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
+ let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
@@ -1086,7 +1096,7 @@ describe User, models: true do
expect(subject.recent_push(project2)).to eq(push_event)
push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
- push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+ push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
end
@@ -1232,7 +1242,7 @@ describe User, models: true do
end
it 'does not include projects for which issues are disabled' do
- project = create(:empty_project, issues_access_level: ProjectFeature::DISABLED)
+ project = create(:empty_project, :issues_disabled)
expect(user.projects_where_can_admin_issues.to_a).to be_empty
expect(user.can?(:admin_issue, project)).to eq(false)
@@ -1422,4 +1432,37 @@ describe User, models: true do
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
end
end
+
+ describe '#access_level=' do
+ let(:user) { build(:user) }
+
+ it 'does nothing for an invalid access level' do
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:regular)
+ expect(user.admin).to be false
+ end
+
+ it "assigns the 'admin' access level" do
+ user.access_level = :admin
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "doesn't clear existing access levels when an invalid access level is passed in" do
+ user.access_level = :admin
+ user.access_level = :invalid_access_level
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+
+ it "accepts string values in addition to symbols" do
+ user.access_level = 'admin'
+
+ expect(user.access_level).to eq(:admin)
+ expect(user.admin).to be true
+ end
+ end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
new file mode 100644
index 00000000000..0f280f32eac
--- /dev/null
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe Ci::BuildPolicy, :models do
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:policies) do
+ described_class.abilities(user, build).to_set
+ end
+
+ shared_context 'public pipelines disabled' do
+ before { project.update_attribute(:public_builds, false) }
+ end
+
+ describe '#rules' do
+ context 'when user does not have access to the project' do
+ let(:project) { create(:empty_project, :private) }
+
+ context 'when public builds are enabled' do
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'when anonymous user has access to the project' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'when team member has access to the project' do
+ let(:project) { create(:empty_project, :public) }
+
+ context 'team member is a guest' do
+ before { project.team << [user, :guest] }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).not_to include :read_build
+ end
+ end
+ end
+
+ context 'team member is a reporter' do
+ before { project.team << [user, :reporter] }
+
+ context 'when public builds are enabled' do
+ it 'includes ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+
+ context 'when public builds are disabled' do
+ include_context 'public pipelines disabled'
+
+ it 'does not include ability to read build' do
+ expect(policies).to include :read_build
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eeab9827d99..0a5edf35f59 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -10,61 +10,59 @@ describe ProjectPolicy, models: true do
let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
let(:guest_permissions) do
- [
- :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
- :read_milestone, :read_project_snippet, :read_project_member,
- :read_note, :create_project, :create_issue, :create_note,
- :upload_file
+ %i[
+ read_project read_board read_list read_wiki read_issue read_label
+ read_milestone read_project_snippet read_project_member
+ read_note create_project create_issue create_note
+ upload_file
]
end
let(:reporter_permissions) do
- [
- :download_code, :fork_project, :create_project_snippet, :update_issue,
- :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
- :read_container_image, :read_pipeline, :read_environment, :read_deployment,
- :read_merge_request, :download_wiki_code
+ %i[
+ download_code fork_project create_project_snippet update_issue
+ admin_issue admin_label admin_list read_commit_status read_build
+ read_container_image read_pipeline read_environment read_deployment
+ read_merge_request download_wiki_code
]
end
let(:team_member_reporter_permissions) do
- [
- :build_download_code, :build_read_container_image
- ]
+ %i[build_download_code build_read_container_image]
end
let(:developer_permissions) do
- [
- :admin_merge_request, :update_merge_request, :create_commit_status,
- :update_commit_status, :create_build, :update_build, :create_pipeline,
- :update_pipeline, :create_merge_request, :create_wiki, :push_code,
- :resolve_note, :create_container_image, :update_container_image,
- :create_environment, :create_deployment
+ %i[
+ admin_merge_request update_merge_request create_commit_status
+ update_commit_status create_build update_build create_pipeline
+ update_pipeline create_merge_request create_wiki push_code
+ resolve_note create_container_image update_container_image
+ create_environment create_deployment
]
end
let(:master_permissions) do
- [
- :push_code_to_protected_branches, :update_project_snippet, :update_environment,
- :update_deployment, :admin_milestone, :admin_project_snippet,
- :admin_project_member, :admin_note, :admin_wiki, :admin_project,
- :admin_commit_status, :admin_build, :admin_container_image,
- :admin_pipeline, :admin_environment, :admin_deployment
+ %i[
+ push_code_to_protected_branches update_project_snippet update_environment
+ update_deployment admin_milestone admin_project_snippet
+ admin_project_member admin_note admin_wiki admin_project
+ admin_commit_status admin_build admin_container_image
+ admin_pipeline admin_environment admin_deployment
]
end
let(:public_permissions) do
- [
- :download_code, :fork_project, :read_commit_status, :read_pipeline,
- :read_container_image, :build_download_code, :build_read_container_image,
- :download_wiki_code
+ %i[
+ download_code fork_project read_commit_status read_pipeline
+ read_container_image build_download_code build_read_container_image
+ download_wiki_code
]
end
let(:owner_permissions) do
- [
- :change_namespace, :change_visibility_level, :rename_project, :remove_project,
- :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+ %i[
+ change_namespace change_visibility_level rename_project remove_project
+ archive_project remove_fork_project destroy_merge_request destroy_issue
]
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
new file mode 100644
index 00000000000..d0758af57dd
--- /dev/null
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe ProjectSnippetPolicy, models: true do
+ let(:current_user) { create(:user) }
+
+ let(:author_permissions) do
+ [
+ :update_project_snippet,
+ :admin_project_snippet
+ ]
+ end
+
+ subject { described_class.abilities(current_user, project_snippet).to_set }
+
+ context 'public snippet' do
+ let(:project_snippet) { create(:project_snippet, :public) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'internal snippet' do
+ let(:project_snippet) { create(:project_snippet, :internal) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+ end
+
+ context 'private snippet' do
+ let(:project_snippet) { create(:project_snippet, :private) }
+
+ context 'no user' do
+ let(:current_user) { nil }
+
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'regular user' do
+ it do
+ is_expected.not_to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'snippet author' do
+ let(:project_snippet) { create(:project_snippet, :private, author: current_user) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+
+ context 'project team member' do
+ before { project_snippet.project.team << [current_user, :developer] }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.not_to include(*author_permissions)
+ end
+ end
+
+ context 'admin user' do
+ let(:current_user) { create(:admin) }
+
+ it do
+ is_expected.to include(:read_project_snippet)
+ is_expected.to include(*author_permissions)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 645e36683bc..834c4e52693 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -67,7 +67,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return project builds' do
+ it 'does not return project builds' do
expect(response).to have_http_status(401)
end
end
@@ -86,7 +86,7 @@ describe API::Builds, api: true do
context 'when commit exists in repository' do
context 'when user is authorized' do
- context 'when pipeline has builds' do
+ context 'when pipeline has jobs' do
before do
create(:ci_pipeline, project: project, sha: project.commit.id)
create(:ci_build, pipeline: pipeline)
@@ -95,7 +95,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
end
- it 'returns project builds for specific commit' do
+ it 'returns project jobs for specific commit' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
@@ -111,7 +111,7 @@ describe API::Builds, api: true do
end
end
- context 'when pipeline has no builds' do
+ context 'when pipeline has no jobs' do
before do
branch_head = project.commit('feature').id
get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user)
@@ -133,7 +133,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
end
- it 'does not return project builds' do
+ it 'does not return project jobs' do
expect(response).to have_http_status(401)
expect(json_response.except('message')).to be_empty
end
@@ -147,7 +147,7 @@ describe API::Builds, api: true do
end
context 'authorized user' do
- it 'returns specific build data' do
+ it 'returns specific job data' do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
@@ -165,7 +165,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build data' do
+ it 'does not return specific job data' do
expect(response).to have_http_status(401)
end
end
@@ -176,7 +176,7 @@ describe API::Builds, api: true do
get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
end
- context 'build with artifacts' do
+ context 'job with artifacts' do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
context 'authorized user' do
@@ -185,22 +185,23 @@ describe API::Builds, api: true do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'returns specific build artifacts' do
+ it 'returns specific job artifacts' do
expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build artifacts' do
+ it 'does not return specific job artifacts' do
expect(response).to have_http_status(401)
end
end
end
- it 'does not return build artifacts if not uploaded' do
+ it 'does not return job artifacts if not uploaded' do
expect(response).to have_http_status(404)
end
end
@@ -241,7 +242,7 @@ describe API::Builds, api: true do
end
end
- context 'non-existing build' do
+ context 'non-existing job' do
shared_examples 'not found' do
it { expect(response).to have_http_status(:not_found) }
end
@@ -254,7 +255,7 @@ describe API::Builds, api: true do
it_behaves_like 'not found'
end
- context 'has no such build' do
+ context 'has no such job' do
before do
get path_for_ref(pipeline.ref, 'NOBUILD')
end
@@ -263,7 +264,7 @@ describe API::Builds, api: true do
end
end
- context 'find proper build' do
+ context 'find proper job' do
shared_examples 'a valid file' do
let(:download_headers) do
{ 'Content-Transfer-Encoding' => 'binary',
@@ -311,7 +312,7 @@ describe API::Builds, api: true do
end
context 'authorized user' do
- it 'returns specific build trace' do
+ it 'returns specific job trace' do
expect(response).to have_http_status(200)
expect(response.body).to eq(build.trace)
end
@@ -320,7 +321,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not return specific build trace' do
+ it 'does not return specific job trace' do
expect(response).to have_http_status(401)
end
end
@@ -333,7 +334,7 @@ describe API::Builds, api: true do
context 'authorized user' do
context 'user with :update_build persmission' do
- it 'cancels running or pending build' do
+ it 'cancels running or pending job' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
@@ -342,7 +343,7 @@ describe API::Builds, api: true do
context 'user without :update_build permission' do
let(:api_user) { reporter.user }
- it 'does not cancel build' do
+ it 'does not cancel job' do
expect(response).to have_http_status(403)
end
end
@@ -351,7 +352,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not cancel build' do
+ it 'does not cancel job' do
expect(response).to have_http_status(401)
end
end
@@ -366,7 +367,7 @@ describe API::Builds, api: true do
context 'authorized user' do
context 'user with :update_build permission' do
- it 'retries non-running build' do
+ it 'retries non-running job' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
@@ -376,7 +377,7 @@ describe API::Builds, api: true do
context 'user without :update_build permission' do
let(:api_user) { reporter.user }
- it 'does not retry build' do
+ it 'does not retry job' do
expect(response).to have_http_status(403)
end
end
@@ -385,7 +386,7 @@ describe API::Builds, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'does not retry build' do
+ it 'does not retry job' do
expect(response).to have_http_status(401)
end
end
@@ -396,23 +397,23 @@ describe API::Builds, api: true do
post api("/projects/#{project.id}/builds/#{build.id}/erase", user)
end
- context 'build is erasable' do
+ context 'job is erasable' do
let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
- it 'erases build content' do
+ it 'erases job content' do
expect(response.status).to eq 201
expect(build.trace).to be_empty
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'updates build' do
+ it 'updates job' do
expect(build.reload.erased_at).to be_truthy
expect(build.reload.erased_by).to eq user
end
end
- context 'build is not erasable' do
+ context 'job is not erasable' do
let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
it 'responds with forbidden' do
@@ -452,20 +453,20 @@ describe API::Builds, api: true do
post api("/projects/#{project.id}/builds/#{build.id}/play", user)
end
- context 'on an playable build' do
+ context 'on an playable job' do
let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
- it 'plays the build' do
+ it 'plays the job' do
expect(response).to have_http_status 200
expect(json_response['user']['id']).to eq(user.id)
expect(json_response['id']).to eq(build.id)
end
end
- context 'on a non-playable build' do
+ context 'on a non-playable job' do
it 'returns a status code 400, Bad Request' do
expect(response).to have_http_status 400
- expect(response.body).to match("Unplayable Build")
+ expect(response.body).to match("Unplayable Job")
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index edbf0140583..f78bde6f53a 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -35,7 +35,8 @@ describe API::Groups, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
- expect(json_response.first['name']).to eq(group1.name)
+ expect(json_response)
+ .to satisfy_one { |group| group['name'] == group1.name }
end
it "does not include statistics" do
@@ -70,7 +71,7 @@ describe API::Groups, api: true do
repository_size: 123,
lfs_objects_size: 234,
build_artifacts_size: 345,
- }
+ }.stringify_keys
project1.statistics.update!(attributes)
@@ -78,7 +79,8 @@ describe API::Groups, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
- expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ expect(json_response)
+ .to satisfy_one { |group| group['statistics'] == attributes }
end
end
@@ -176,6 +178,10 @@ describe API::Groups, api: true do
expect(json_response['visibility_level']).to eq(group1.visibility_level)
expect(json_response['avatar_url']).to eq(group1.avatar_url)
expect(json_response['web_url']).to eq(group1.web_url)
+ expect(json_response['request_access_enabled']).to eq(group1.request_access_enabled)
+ expect(json_response['full_name']).to eq(group1.full_name)
+ expect(json_response['full_path']).to eq(group1.full_path)
+ expect(json_response['parent_id']).to eq(group1.parent_id)
expect(json_response['projects']).to be_an Array
expect(json_response['projects'].length).to eq(2)
expect(json_response['shared_projects']).to be_an Array
@@ -323,7 +329,7 @@ describe API::Groups, api: true do
expect(response).to have_http_status(404)
end
- it "should only return projects to which user has access" do
+ it "only returns projects to which user has access" do
project3.team << [user3, :developer]
get api("/groups/#{group1.id}/projects", user3)
@@ -335,7 +341,7 @@ describe API::Groups, api: true do
end
context "when authenticated as admin" do
- it "should return any existing group" do
+ it "returns any existing group" do
get api("/groups/#{group2.id}/projects", admin)
expect(response).to have_http_status(200)
@@ -343,7 +349,7 @@ describe API::Groups, api: true do
expect(json_response.first['name']).to eq(project2.name)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328/projects", admin)
expect(response).to have_http_status(404)
@@ -351,7 +357,7 @@ describe API::Groups, api: true do
end
context 'when using group path in URL' do
- it 'should return any existing group' do
+ it 'returns any existing group' do
get api("/groups/#{group1.path}/projects", admin)
expect(response).to have_http_status(200)
@@ -395,6 +401,19 @@ describe API::Groups, api: true do
expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
end
+ it "creates a nested group" do
+ parent = create(:group)
+ parent.add_owner(user3)
+ group = attributes_for(:group, { parent_id: parent.id })
+
+ post api("/groups", user3), group
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response["full_path"]).to eq("#{parent.path}/#{group[:path]}")
+ expect(json_response["parent_id"]).to eq(parent.id)
+ end
+
it "does not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 62f1b8d7ca2..cca00df9591 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -425,7 +425,7 @@ describe API::Issues, api: true do
end
it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ restricted_project = create(:empty_project, :public, :issues_private)
create(:issue, project: restricted_project)
get api("/projects/#{restricted_project.id}/issues", non_member)
@@ -612,23 +612,6 @@ describe API::Issues, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
- it 'returns a project issue by iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 1
- expect(json_response.first['title']).to eq issue.title
- expect(json_response.first['id']).to eq issue.id
- expect(json_response.first['iid']).to eq issue.iid
- end
-
- it 'returns an empty array for an unknown project issue iid' do
- get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
-
- expect(response.status).to eq 200
- expect(json_response.length).to eq 0
- end
-
it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 9892e014cb9..3e9bcfd1a60 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -145,11 +145,11 @@ describe API::Members, api: true do
end
end
- it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ it "returns 409 if member already exists" do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: master.id, access_level: Member::MASTER
- expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ expect(response).to have_http_status(409)
end
it 'returns 400 when user_id is not given' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 21a2c583aa8..ff10e79e417 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -73,6 +73,16 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
+ it 'returns merge_request by "iids" array' do
+ get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
context "with ordering" do
before do
@mr_later = mr_with_later_created_and_updated_at_time
@@ -159,24 +169,6 @@ describe API::MergeRequests, api: true do
expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'returns merge_request by iid' do
- url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
- get api(url, user)
- expect(response.status).to eq 200
- expect(json_response.first['title']).to eq merge_request.title
- expect(json_response.first['id']).to eq merge_request.id
- end
-
- it 'returns merge_request by iid array' do
- get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- expect(json_response.first['title']).to eq merge_request_closed.title
- expect(json_response.first['id']).to eq merge_request_closed.id
- end
-
it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 01032c0929b..eea76c7bb94 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -4,25 +4,14 @@ describe API::ProjectSnippets, api: true do
include ApiHelpers
let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
let(:admin) { create(:admin) }
- describe 'GET /projects/:project_id/snippets/:id' do
- # TODO (rspeicher): Deprecated; remove in 9.0
- it 'always exposes expires_at as nil' do
- snippet = create(:project_snippet, author: admin)
-
- get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
-
- expect(json_response).to have_key('expires_at')
- expect(json_response['expires_at']).to be_nil
- end
- end
-
describe 'GET /projects/:project_id/snippets/' do
let(:user) { create(:user) }
it 'returns all snippets available to team member' do
- project.team << [user, :developer]
+ project.add_developer(user)
public_snippet = create(:project_snippet, :public, project: project)
internal_snippet = create(:project_snippet, :internal, project: project)
private_snippet = create(:project_snippet, :private, project: project)
@@ -50,7 +39,7 @@ describe API::ProjectSnippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
code: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -72,6 +61,51 @@ describe API::ProjectSnippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
end
describe 'PUT /projects/:project_id/snippets/:id/' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a1db81ce18c..ac0bbec44e0 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -359,13 +359,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
- post api('/projects', user), project
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-
it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api('/projects', user), project
@@ -373,13 +366,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'sets a project as internal overriding :public' do
- project = attributes_for(:project, :internal, { public: true })
- post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
it 'sets a project as private' do
project = attributes_for(:project, :private)
post api('/projects', user), project
@@ -387,13 +373,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
- post api('/projects', user), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
post api('/projects', user), project
@@ -431,13 +410,14 @@ describe API::Projects, api: true do
end
context 'when a visibility level is restricted' do
+ let(:project_param) { attributes_for(:project, :public) }
+
before do
- @project = attributes_for(:project, { public: true })
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
it 'does not allow a non-admin to use a restricted visibility level' do
- post api('/projects', user), @project
+ post api('/projects', user), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['visibility_level'].first).to(
@@ -446,7 +426,8 @@ describe API::Projects, api: true do
end
it 'allows an admin to override restricted visibility settings' do
- post api('/projects', admin), @project
+ post api('/projects', admin), project_param
+
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to(
eq(Gitlab::VisibilityLevel::PUBLIC)
@@ -459,7 +440,7 @@ describe API::Projects, api: true do
before { project }
before { admin }
- it 'should create new project without path and return 201' do
+ it 'creates new project without path and return 201' do
expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
end
@@ -499,15 +480,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'sets a project as public using :public' do
- project = attributes_for(:project, { public: true })
- post api("/projects/user/#{user.id}", admin), project
-
- expect(response).to have_http_status(201)
- expect(json_response['public']).to be_truthy
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-
it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api("/projects/user/#{user.id}", admin), project
@@ -517,14 +489,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'sets a project as internal overriding :public' do
- project = attributes_for(:project, :internal, { public: true })
- post api("/projects/user/#{user.id}", admin), project
- expect(response).to have_http_status(201)
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
-
it 'sets a project as private' do
project = attributes_for(:project, :private)
post api("/projects/user/#{user.id}", admin), project
@@ -532,13 +496,6 @@ describe API::Projects, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'sets a project as private using :public' do
- project = attributes_for(:project, { public: false })
- post api("/projects/user/#{user.id}", admin), project
- expect(json_response['public']).to be_falsey
- expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
-
it 'sets a project as allowing merge even if build fails' do
project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
post api("/projects/user/#{user.id}", admin), project
@@ -865,7 +822,7 @@ describe API::Projects, api: true do
it 'creates a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
title: 'api test', file_name: 'sample.rb', code: 'test',
- visibility_level: '0'
+ visibility_level: Gitlab::VisibilityLevel::PRIVATE
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('api test')
end
@@ -1085,52 +1042,6 @@ describe API::Projects, api: true do
end
end
- describe 'GET /projects/search/:query' do
- let!(:query) { 'query'}
- let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
- let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
- let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
- let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
- let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
- let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
- let!(:public) { create(:empty_project, :public, name: "public #{query}") }
- let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
- let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
-
- shared_examples_for 'project search response' do |args = {}|
- it 'returns project search responses' do
- get api("/projects/search/#{args[:query]}", current_user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(args[:results])
- json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project search response', query: 'query', results: 1 do
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- it_behaves_like 'project search response', query: 'query', results: 6 do
- let(:current_user) { user }
- end
- it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
- let(:current_user) { user }
- end
- end
-
- context 'when authenticated as a different user' do
- it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
- let(:current_user) { user2 }
- end
- end
- end
-
describe 'PUT /projects/:id' do
before { project }
before { user }
@@ -1160,7 +1071,7 @@ describe API::Projects, api: true do
end
it 'updates visibility_level' do
- project_param = { visibility_level: 20 }
+ project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
@@ -1170,7 +1081,7 @@ describe API::Projects, api: true do
it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
- project_param = { public: false }
+ project_param = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
project_param.each_pair do |k, v|
@@ -1243,7 +1154,7 @@ describe API::Projects, api: true do
end
it 'does not update visibility_level' do
- project_param = { visibility_level: 20 }
+ project_param = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index f6fb6ea5506..6b9a739b439 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -80,7 +80,7 @@ describe API::Snippets, api: true do
title: 'Test Title',
file_name: 'test.rb',
content: 'puts "hello world"',
- visibility_level: Gitlab::VisibilityLevel::PUBLIC
+ visibility_level: Snippet::PUBLIC
}
end
@@ -101,6 +101,36 @@ describe API::Snippets, api: true do
expect(response).to have_http_status(400)
end
+
+ context 'when the snippet is spam' do
+ def create_snippet(snippet_params = {})
+ post api('/snippets', user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
end
describe 'PUT /snippets/:id' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 5bf5bf0739e..5958012672e 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -190,6 +190,18 @@ describe API::Users, api: true do
expect(new_user.external).to be_truthy
end
+ it "creates user with reset password" do
+ post api('/users', admin), attributes_for(:user, reset_password: true).except(:password)
+
+ expect(response).to have_http_status(201)
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+
+ expect(new_user).not_to eq(nil)
+ expect(new_user.recently_sent_password_reset?).to eq(true)
+ end
+
it "does not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
@@ -305,6 +317,13 @@ describe API::Users, api: true do
expect(user.reload.bio).to eq('new test bio')
end
+ it "updates user with new password and forces reset on next login" do
+ put api("/users/#{user.id}", admin), password: '12345678'
+
+ expect(response).to have_http_status(200)
+ expect(user.reload.password_expires_at).to be <= Time.now
+ end
+
it "updates user with organization" do
put api("/users/#{user.id}", admin), { organization: 'GitLab' }
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
new file mode 100644
index 00000000000..f5bdf408c5e
--- /dev/null
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe API::V3::DeployKeys, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id) }
+ let(:project2) { create(:empty_project, creator_id: user.id) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ describe 'GET /deploy_keys' do
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get v3_api('/deploy_keys')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 403 error' do
+ get v3_api('/deploy_keys', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'should return all deploy keys' do
+ get v3_api('/deploy_keys', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+ end
+ end
+ end
+
+ %w(deploy_keys keys).each do |path|
+ describe "GET /projects/:id/#{path}" do
+ before { deploy_key }
+
+ it 'should return array of ssh keys' do
+ get v3_api("/projects/#{project.id}/#{path}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(deploy_key.title)
+ end
+ end
+
+ describe "GET /projects/:id/#{path}/:key_id" do
+ it 'should return a single key' do
+ get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(deploy_key.title)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ get v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/deploy_keys" do
+ it 'should not create an invalid ssh key' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'should not create a key without title' do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('title is missing')
+ end
+
+ it 'should create new ssh key' do
+ key_attrs = attributes_for :another_key
+
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
+ end.to change{ project.deploy_keys.count }.by(1)
+ end
+
+ it 'returns an existing ssh key when attempting to add a duplicate' do
+ expect do
+ post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.not_to change { project.deploy_keys.count }
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'joins an existing ssh key to a new project' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.to change { project2.deploy_keys.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ describe "DELETE /projects/:id/#{path}/:key_id" do
+ before { deploy_key }
+
+ it 'should delete existing key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
+ end.to change{ project.deploy_keys.count }.by(-1)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ delete v3_api("/projects/#{project.id}/#{path}/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/#{path}/:key_id/enable" do
+ let(:project2) { create(:empty_project) }
+
+ context 'when the user can admin the project' do
+ it 'enables the key' do
+ expect do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
+ end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
+ context 'when the user can admin the project' do
+ it 'disables the key' do
+ expect do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
+ end.to change { project.deploy_keys.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
new file mode 100644
index 00000000000..33a127de98a
--- /dev/null
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -0,0 +1,1259 @@
+require 'spec_helper'
+
+describe API::V3::Issues, api: true do
+ include ApiHelpers
+ include EmailHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 3.hours.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee,
+ created_at: generate(:issue_created_at),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: project,
+ milestone: milestone,
+ created_at: generate(:issue_created_at),
+ updated_at: 1.hour.ago
+ end
+ let!(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let!(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let!(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { URI.escape(Milestone::None.title) }
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /issues" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/issues")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of issues" do
+ get v3_api("/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'returns an array of closed issues' do
+ get v3_api('/issues?state=closed', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of opened issues' do
+ get v3_api('/issues?state=opened', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get v3_api('/issues?state=all', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of labeled issues' do
+ get v3_api("/issues?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues when at least one label matches' do
+ get v3_api("/issues?labels=#{label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get v3_api('/issues?labels=foo,bar', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled issues matching given state' do
+ get v3_api("/issues?labels=#{label.title}&state=opened", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an empty array if no issue matches labels and state filters' do
+ get v3_api("/issues?labels=#{label.title}&state=closed", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api('/issues', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api('/issues?sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api('/issues?order_by=updated_at', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api('/issues?order_by=updated_at&sort=asc', user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+
+ describe "GET /groups/:id/issues" do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:empty_project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignee: assignee,
+ updated_at: 2.hours.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignee: user,
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ before do
+ group_project.team << [user, :reporter]
+ end
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ it 'returns group issues without confidential issues for non project members' do
+ get v3_api(base_url, non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(group_issue.title)
+ end
+
+ it 'returns group confidential issues for author' do
+ get v3_api(base_url, author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get v3_api(base_url, assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get v3_api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns group confidential issues for admin' do
+ get v3_api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+
+ it 'returns an array of labeled group issues' do
+ get v3_api("#{base_url}?labels=#{group_label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no group issue matches labels' do
+ get v3_api("#{base_url}?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}?milestone=#{group_milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(group_confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api(base_url, user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ it "returns 404 on private projects for other users" do
+ private_project = create(:empty_project, :private)
+ create(:issue, project: private_project)
+
+ get v3_api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+ create(:issue, project: restricted_project)
+
+ get v3_api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect(json_response).to eq([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get v3_api("#{base_url}/issues", non_member)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get v3_api("#{base_url}/issues", guest)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for author' do
+ get v3_api("#{base_url}/issues", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get v3_api("#{base_url}/issues", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get v3_api("#{base_url}/issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns project confidential issues for admin' do
+ get v3_api("#{base_url}/issues", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'returns an array of labeled project issues' do
+ get v3_api("#{base_url}/issues?labels=#{label.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled project issues where all labels match' do
+ get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get v3_api("#{base_url}/issues?labels=foo,bar", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get v3_api("#{base_url}/issues?milestone=foo", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.second['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user),
+ '&state=closed'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(confidential_issue.id)
+ end
+
+ it 'sorts by created_at descending by default' do
+ get v3_api("#{base_url}/issues", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts ascending when requested' do
+ get v3_api("#{base_url}/issues?sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['created_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
+
+ response_dates = json_response.map { |issue| issue['updated_at'] }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ describe "GET /projects/:id/issues/:issue_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(issue.id)
+ expect(json_response['iid']).to eq(issue.iid)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['description']).to eq(issue.description)
+ expect(json_response['state']).to eq(issue.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(issue.label_names)
+ expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it "returns a project issue by id" do
+ get v3_api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns a project issue by iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 1
+ expect(json_response.first['title']).to eq issue.title
+ expect(json_response.first['id']).to eq issue.id
+ expect(json_response.first['iid']).to eq issue.iid
+ end
+
+ it 'returns an empty array for an unknown project issue iid' do
+ get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user)
+
+ expect(response.status).to eq 200
+ expect(json_response.length).to eq 0
+ end
+
+ it "returns 404 if issue id not found" do
+ get v3_api("/projects/#{project.id}/issues/54321", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it "returns 404 for non project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 for project members with guest role" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns confidential issue for project members" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for author" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for assignee" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "returns confidential issue for admin" do
+ get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
+ end
+
+ describe "POST /projects/:id/issues" do
+ it 'creates a new project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a new confidential project issue' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'y'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: false
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2, project)
+
+ perform_enqueued_jobs do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it "returns a 400 bad request if title not given" do
+ post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving issues in a merge request' do
+ let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first }
+ let(:merge_request) { discussion.noteable }
+ let(:project) { merge_request.source_project }
+ before do
+ project.team << [user, :master]
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'New Issue',
+ merge_request_for_resolving_discussions: merge_request.iid
+ end
+
+ it 'creates a new project issue' do
+ expect(response).to have_http_status(:created)
+ end
+
+ it 'resolves the discussions in a merge request' do
+ discussion.first_note.reload
+
+ expect(discussion.resolved?).to be(true)
+ end
+
+ it 'assigns a description to the issue mentioning the merge request' do
+ expect(json_response['description']).to include(merge_request.to_reference)
+ end
+ end
+
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the creation date to be set' do
+ creation_time = 2.weeks.ago
+ post v3_api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: 'label, label2', created_at: creation_time
+
+ expect(response).to have_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2'
+ end.not_to change { project.labels.count }
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it "does not create a new project issue" do
+ expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq({ "error" => "Spam detected" })
+
+ spam_logs = SpamLog.all
+
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update only title" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "returns 404 error if issue id not found" do
+ put v3_api("/projects/#{project.id}/issues/44444", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'confidential issues' do
+ it "returns 403 for non project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
+ title: 'updated title'
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "updates a confidential issue for project members" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for author" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "updates a confidential issue for admin" do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'sets an issue to confidential' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ confidential: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ confidential: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id to update labels' do
+ let!(:label) { create(:label, title: 'dummy', project: project) }
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+
+ it 'does not update labels if not present' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ 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, project)
+
+ perform_enqueued_jobs do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
+ it 'removes all labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: ''
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'updates labels' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'foo,bar'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'g' * 256
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+ end
+
+ describe "PUT /projects/:id/issues/:issue_id to update state and label" do
+ it "updates a project issue" do
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label2', state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['state']).to eq "closed"
+ end
+
+ it 'reopens a project isssue' do
+ put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'reopened'
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the update date to be set' do
+ update_time = 2.weeks.ago
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user),
+ labels: 'label3', state_event: 'close', updated_at: update_time
+
+ expect(response).to have_http_status(200)
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ describe "DELETE /projects/:id/issues/:issue_id" do
+ it "rejects a non member from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a developer from deleting an issue" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author)
+
+ expect(response).to have_http_status(403)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ it "deletes the issue if an admin requests it" do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete v3_api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_id/move' do
+ let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) }
+
+ it 'moves an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project.id)
+ end
+
+ context 'when source and target projects are the same' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: project.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
+ end
+ end
+
+ context 'when the user does not have the permission to move issues' do
+ it 'returns 400 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
+ end
+ end
+
+ it 'moves the issue to another namespace if I am admin' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin),
+ to_project_id: target_project2.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['project_id']).to eq(target_project2.id)
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/123/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when source project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/123/issues/#{issue.id}/move", user),
+ to_project_id: target_project.id
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ to_project_id: 123
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/subscription' do
+ it 'subscribes to an issue' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE :id/issues/:issue_id/subscription' do
+ it 'unsubscribes from an issue' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ delete v3_api("/projects/#{project.id}/issues/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
+end
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
new file mode 100644
index 00000000000..28c3ca03960
--- /dev/null
+++ b/spec/requests/api/v3/members_spec.rb
@@ -0,0 +1,342 @@
+require 'spec_helper'
+
+describe API::Members, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ create(:empty_project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
+ project.team << [developer, :developer]
+ project.team << [master, :master]
+ project.request_access(access_requester)
+ end
+ end
+
+ let!(:group) do
+ 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|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+ end
+ end
+
+ it 'does not return invitees' do
+ create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
+
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['id'] }).to match_array [master.id, developer.id]
+ end
+
+ it 'finds members with query string' do
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+ expect(response).to have_http_status(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['username']).to eq(master.username)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(200)
+ # User attributes
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['name']).to eq(developer.name)
+ expect(json_response['username']).to eq(developer.username)
+ expect(json_response['state']).to eq(developer.state)
+ expect(json_response['avatar_url']).to eq(developer.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+ user_id: access_requester.id, access_level: Member::MASTER
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", user),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and new member is already a requester' do
+ it 'transforms the requester into a proper member' do
+ expect do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(source.requesters.count).to eq(0)
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+ end
+
+ it 'creates a new member' do
+ expect do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: master.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ end
+
+ it 'returns 400 when user_id is not given' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access_level is not valid' do
+ post v3_api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+ access_level: Member::MASTER
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: Member::MASTER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it 'returns 409 if member does not exist' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access level is not valid' do
+ put v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a member and deleting themself' do
+ it 'deletes the member' do
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and member is a requester' do
+ it "returns #{source_type == 'project' ? 200 : 404}" do
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ it 'deletes the member' do
+ expect do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ delete v3_api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ context 'Adding owner to project' do
+ it 'returns 403' do
+ expect do
+ post v3_api("/projects/#{project.id}/members", master),
+ user_id: stranger.id, access_level: Member::OWNER
+
+ expect(response).to have_http_status(422)
+ end.to change { project.members.count }.by(0)
+ end
+ end
+end
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
new file mode 100644
index 00000000000..b94e1ef4ced
--- /dev/null
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -0,0 +1,726 @@
+require "spec_helper"
+
+describe API::MergeRequests, api: true do
+ include ApiHelpers
+ let(:base_time) { Time.now }
+ let(:user) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let(:non_member) { create(:user) }
+ let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+
+ before do
+ project.team << [user, :reporter]
+ end
+
+ describe "GET /projects/:id/merge_requests" do
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get v3_api("/projects/#{project.id}/merge_requests")
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+
+ it "returns an array of all merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of open merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=opened", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it "returns an array of closed merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=closed", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_closed.title)
+ end
+
+ it "returns an array of merged merge_requests" do
+ get v3_api("/projects/#{project.id}/merge_requests?state=merged", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ end
+
+ context "with ordering" do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it "returns an array of merge_requests in ascending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it "returns an array of merge_requests in descending order" do
+ get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by updated_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it "returns an array of merge_requests ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+ end
+ end
+
+ describe "GET /projects/:id/merge_requests/:merge_request_id" do
+ it 'exposes known attributes' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(merge_request.id)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['project_id']).to eq(merge_request.project.id)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['description']).to eq(merge_request.description)
+ expect(json_response['state']).to eq(merge_request.state)
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(merge_request.label_names)
+ expect(json_response['milestone']).to be_nil
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_branch']).to eq(merge_request.target_branch)
+ expect(json_response['source_branch']).to eq(merge_request.source_branch)
+ expect(json_response['upvotes']).to eq(0)
+ expect(json_response['downvotes']).to eq(0)
+ expect(json_response['source_project_id']).to eq(merge_request.source_project.id)
+ expect(json_response['target_project_id']).to eq(merge_request.target_project.id)
+ expect(json_response['work_in_progress']).to be_falsy
+ expect(json_response['merge_when_build_succeeds']).to be_falsy
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(merge_request.title)
+ expect(json_response['iid']).to eq(merge_request.iid)
+ expect(json_response['work_in_progress']).to eq(false)
+ expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
+ end
+
+ it 'returns merge_request by iid' do
+ url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
+ get v3_api(url, user)
+ expect(response.status).to eq 200
+ expect(json_response.first['title']).to eq merge_request.title
+ expect(json_response.first['id']).to eq merge_request.id
+ end
+
+ it 'returns merge_request by iid array' do
+ get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq merge_request_closed.title
+ expect(json_response.first['id']).to eq merge_request_closed.id
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'Work in Progress' do
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+
+ it "returns merge_request" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['work_in_progress']).to eq(true)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do
+ it 'returns a 200 when merge request is valid' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
+ commit = merge_request.commits.first
+
+ expect(response.status).to eq 200
+ expect(json_response.size).to eq(merge_request.commits.size)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(json_response.first['title']).to eq(commit.title)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/commits", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
+ it 'returns the change information of the merge_request' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
+ expect(response.status).to eq 200
+ expect(json_response['changes'].size).to eq(merge_request.diffs.size)
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get v3_api("/projects/#{project.id}/merge_requests/999/changes", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests" do
+ context 'between branches projects' do
+ it "returns merge_request" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label2',
+ milestone_id: milestone.id,
+ remove_source_branch: true
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['labels']).to eq(['label', 'label2'])
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it "returns 422 when source_branch equals target_branch" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
+ expect(response).to have_http_status(422)
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", target_branch: "master", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: "Test merge_request", source_branch: "markdown", author: user
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ target_branch: 'master', source_branch: 'markdown'
+ expect(response).to have_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'markdown',
+ target_branch: 'master',
+ author: user,
+ labels: 'label, label?, label&foo, ?, &'
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'with existing MR' do
+ before do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ @mr = MergeRequest.all.last
+ end
+
+ it 'returns 409 when MR already exists for source/target' do
+ expect do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'New test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author: user
+ end.to change { MergeRequest.count }.by(0)
+ expect(response).to have_http_status(409)
+ end
+ end
+ end
+
+ context 'forked projects' do
+ let!(:user2) { create(:user) }
+ let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
+ let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) }
+
+ before :each do |each|
+ fork_project.team << [user2, :reporter]
+ end
+
+ it "returns merge_request" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
+ author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['description']).to eq('Test description for Test merge_request')
+ end
+
+ it "does not return 422 when source_branch equals target_branch" do
+ expect(project.id).not_to eq(fork_project.id)
+ expect(fork_project.forked?).to be_truthy
+ expect(fork_project.forked_from_project).to eq(project)
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ end
+
+ it "returns 400 when source_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when target_branch is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 400 when title is missing" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when target_branch is specified' do
+ it 'returns 422 if not a forked project' do
+ post v3_api("/projects/#{project.id}/merge_requests", user),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user,
+ target_project_id: fork_project.id
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns 422 if targeting a different fork' do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: unrelated_project.id
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ it "returns 201 when target_branch is specified and for the same project" do
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
+
+ describe "DELETE /projects/:id/merge_requests/:merge_request_id" do
+ context "when the user is developer" do
+ let(:developer) { create(:user) }
+
+ before do
+ project.team << [developer, :developer]
+ end
+
+ it "denies the deletion of the merge request" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context "when the user is project owner" do
+ it "destroys the merge request owners can destroy" do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
+ let(:pipeline) { create(:ci_pipeline_without_jobs) }
+
+ it "returns merge_request in case of success" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 406 if branch can't be merged" do
+ allow_any_instance_of(MergeRequest).
+ to receive(:can_be_merged?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(406)
+ expect(json_response['message']).to eq('Branch cannot be merged')
+ end
+
+ it "returns 405 if merge_request is not open" do
+ merge_request.close
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 405 if merge_request is a work in progress" do
+ merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it 'returns 405 if the build failed for a merge request that requires success' do
+ allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
+
+ expect(response).to have_http_status(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
+ it "returns 401 if user has no permissions to merge" do
+ user2 = create(:user)
+ project.team << [user2, :reporter]
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
+ expect(response).to have_http_status(401)
+ expect(json_response['message']).to eq('401 Unauthorized')
+ end
+
+ it "returns 409 if the SHA parameter doesn't match" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse
+
+ expect(response).to have_http_status(409)
+ expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
+ end
+
+ it "succeeds if the SHA parameter matches" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "enables merge when pipeline succeeds if the pipeline is active" do
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow(pipeline).to receive(:active?).and_return(true)
+
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_build_succeeds']).to eq(true)
+ end
+ end
+
+ describe "PUT /projects/:id/merge_requests/:merge_request_id" do
+ context "to close a MR" do
+ it "returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
+
+ expect(response).to have_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ end
+ end
+
+ it "updates title and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title"
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('New title')
+ end
+
+ it "updates description and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description"
+ expect(response).to have_http_status(200)
+ expect(json_response['description']).to eq('New description')
+ end
+
+ it "updates milestone_id and returns merge_request" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id
+ expect(response).to have_http_status(200)
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ end
+
+ it "returns merge_request with renamed target_branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki"
+ expect(response).to have_http_status(200)
+ expect(json_response['target_branch']).to eq('wiki')
+ end
+
+ it "returns merge_request that removes the source branch" do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+
+ it 'allows special label names' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user),
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'does not update state when title is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+
+ it 'does not update state when target_branch is empty' do
+ put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil
+
+ merge_request.reload
+ expect(response).to have_http_status(400)
+ expect(merge_request.state).to eq('opened')
+ end
+ end
+
+ describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
+ it "returns comment" do
+ original_count = merge_request.notes.size
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['author']['name']).to eq(user.name)
+ expect(json_response['author']['username']).to eq(user.username)
+ expect(merge_request.reload.notes.size).to eq(original_count + 1)
+ end
+
+ it "returns 400 if note is missing" do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 404 if note is attached to non existent merge request" do
+ post v3_api("/projects/#{project.id}/merge_requests/404/comments", user),
+ note: 'My comment'
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET :id/merge_requests/:merge_request_id/comments" do
+ let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
+ let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
+
+ it "returns merge_request comments ordered by created_at" do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['note']).to eq("a comment on a MR")
+ expect(json_response.first['author']['id']).to eq(user.id)
+ expect(json_response.last['note']).to eq("another comment on a MR")
+ end
+
+ it "returns a 404 error if merge_request_id not found" do
+ get v3_api("/projects/#{project.id}/merge_requests/999/comments", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do
+ it 'returns the issue that will be closed on merge' do
+ issue = create(:issue, project: project)
+ mr = merge_request.tap do |mr|
+ mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}")
+ end
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns an empty array when there are no issues to be closed' do
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'handles external issues' do
+ jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
+ issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
+ merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+
+ get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns 403 if the user has no access to the merge request' do
+ project = create(:empty_project, :private)
+ merge_request = create(:merge_request, :simple, source_project: project)
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'POST :id/merge_requests/:merge_request_id/subscription' do
+ it 'subscribes to a merge request' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
+ it 'unsubscribes from a merge request' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
+
+ expect(response).to have_http_status(304)
+ end
+
+ it 'returns 404 if the merge request is not found' do
+ post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 403 if user has no access to read code' do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
+ delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe 'Time tracking' do
+ let(:issuable) { merge_request }
+
+ include_examples 'time tracking endpoints', 'merge_request'
+ end
+
+ def mr_with_later_created_and_updated_at_time
+ merge_request
+ merge_request.created_at += 1.hour
+ merge_request.updated_at += 30.minutes
+ merge_request.save
+ merge_request
+ end
+
+ def mr_with_earlier_created_and_updated_at_time
+ merge_request_closed
+ merge_request_closed.created_at -= 1.hour
+ merge_request_closed.updated_at -= 30.minutes
+ merge_request_closed.save
+ merge_request_closed
+ end
+end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
new file mode 100644
index 00000000000..3700477f0db
--- /dev/null
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -0,0 +1,188 @@
+require 'rails_helper'
+
+describe API::ProjectSnippets, api: true do
+ include ApiHelpers
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET /projects/:project_id/snippets/:id' do
+ # TODO (rspeicher): Deprecated; remove in 9.0
+ it 'always exposes expires_at as nil' do
+ snippet = create(:project_snippet, author: admin)
+
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin)
+
+ expect(json_response).to have_key('expires_at')
+ expect(json_response['expires_at']).to be_nil
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/' do
+ let(:user) { create(:user) }
+
+ it 'returns all snippets available to team member' do
+ project.add_developer(user)
+ public_snippet = create(:project_snippet, :public, project: project)
+ internal_snippet = create(:project_snippet, :internal, project: project)
+ private_snippet = create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(3)
+ expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'hides private snippets from regular user' do
+ create(:project_snippet, :private, project: project)
+
+ get v3_api("/projects/#{project.id}/snippets/", user)
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ describe 'POST /projects/:project_id/snippets/' do
+ let(:params) do
+ {
+ title: 'Test Title',
+ file_name: 'test.rb',
+ code: 'puts "hello world"',
+ visibility_level: Snippet::PUBLIC
+ }
+ end
+
+ it 'creates a new snippet' do
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(201)
+ snippet = ProjectSnippet.find(json_response['id'])
+ expect(snippet.content).to eq(params[:code])
+ expect(snippet.title).to eq(params[:title])
+ expect(snippet.file_name).to eq(params[:file_name])
+ expect(snippet.visibility_level).to eq(params[:visibility_level])
+ end
+
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
+
+ post v3_api("/projects/#{project.id}/snippets/", admin), params
+
+ expect(response).to have_http_status(400)
+ end
+
+ context 'when the snippet is spam' do
+ def create_snippet(project, snippet_params = {})
+ project.add_developer(user)
+
+ post v3_api("/projects/#{project.id}/snippets", user), params.merge(snippet_params)
+ end
+
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ context 'when the project is private' do
+ let(:private_project) { create(:project_empty_repo, :private) }
+
+ context 'when the snippet is public' do
+ it 'creates the snippet' do
+ expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+ end
+
+ context 'when the project is public' do
+ context 'when the snippet is private' do
+ it 'creates the snippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }.
+ to change { Snippet.count }.by(1)
+ end
+ end
+
+ context 'when the snippet is public' do
+ it 'rejects the shippet' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ not_to change { Snippet.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it 'creates a spam log' do
+ expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }.
+ to change { SpamLog.count }.by(1)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'PUT /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'updates snippet' do
+ new_content = 'New content'
+
+ put v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content
+
+ expect(response).to have_http_status(200)
+ snippet.reload
+ expect(snippet.content).to eq(new_content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ put v3_api("/projects/#{snippet.project.id}/snippets/1234", admin), title: 'foo'
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ put v3_api("/projects/#{project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'DELETE /projects/:project_id/snippets/:id/' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'deletes snippet' do
+ admin = create(:admin)
+ snippet = create(:project_snippet, author: admin)
+
+ delete v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
+ describe 'GET /projects/:project_id/snippets/:id/raw' do
+ let(:snippet) { create(:project_snippet, author: admin) }
+
+ it 'returns raw text' do
+ get v3_api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'text/plain'
+ expect(response.body).to eq(snippet.content)
+ end
+
+ it 'returns 404 for invalid snippet id' do
+ delete v3_api("/projects/#{snippet.project.id}/snippets/1234", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
new file mode 100644
index 00000000000..a495122bba7
--- /dev/null
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -0,0 +1,1424 @@
+require 'spec_helper'
+
+describe API::V3::Projects, api: true do
+ include ApiHelpers
+ include Gitlab::CurrentSettings
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
+ let(:project_member) { create(:project_member, :master, user: user, project: project) }
+ let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
+ let(:user4) { create(:user) }
+ let(:project3) do
+ create(:project,
+ :private,
+ :repository,
+ name: 'second_project',
+ path: 'second_project',
+ creator_id: user.id,
+ namespace: user.namespace,
+ merge_requests_enabled: false,
+ issues_enabled: false, wiki_enabled: false,
+ snippets_enabled: false)
+ end
+ let(:project_member3) do
+ create(:project_member,
+ user: user4,
+ project: project3,
+ access_level: ProjectMember::MASTER)
+ end
+ let(:project4) do
+ create(:empty_project,
+ name: 'third_project',
+ path: 'third_project',
+ creator_id: user4.id,
+ namespace: user4.namespace)
+ end
+
+ describe 'GET /projects' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns an array of projects' do
+ get v3_api('/projects', user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(project.name)
+ expect(json_response.first['owner']['username']).to eq(user.username)
+ end
+
+ it 'includes the project labels as the tag_list' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('tag_list')
+ end
+
+ it 'includes open_issues_count' do
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('open_issues_count')
+ end
+
+ it 'does not include open_issues_count' do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
+ get v3_api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).not_to include('open_issues_count')
+ end
+
+ context 'GET /projects?simple=true' do
+ it 'returns a simplified version of all the projects' do
+ expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+
+ get v3_api('/projects?simple=true', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to match_array expected_keys
+ end
+ end
+
+ context 'and using search' do
+ it 'returns searched project' do
+ get v3_api('/projects', user), { search: project.name }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the visibility filter' do
+ it 'filters based on private visibility param' do
+ get v3_api('/projects', user), { visibility: 'private' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
+ end
+
+ it 'filters based on internal visibility param' do
+ get v3_api('/projects', user), { visibility: 'internal' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
+ end
+
+ it 'filters based on public visibility param' do
+ get v3_api('/projects', user), { visibility: 'public' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count)
+ end
+ end
+
+ context 'and using sorting' do
+ before do
+ project2
+ project3
+ end
+
+ it 'returns the correct order when sorted by id' do
+ get v3_api('/projects', user), { order_by: 'id', sort: 'desc' }
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(project3.id)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/all' do
+ before { project }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all')
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as regular user' do
+ it 'returns authentication error' do
+ get v3_api('/projects/all', user)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'returns an array of all projects' do
+ get v3_api('/projects/all', admin)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+
+ expect(json_response).to satisfy do |response|
+ response.one? do |entry|
+ entry.has_key?('permissions') &&
+ entry['name'] == project.name &&
+ entry['owner']['username'] == user.username
+ end
+ end
+ end
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/all', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ get v3_api('/projects/all', admin), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).to include 'statistics'
+ end
+ end
+ end
+
+ describe 'GET /projects/owned' do
+ before do
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get v3_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 v3_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
+
+ it "does not include statistics by default" do
+ get v3_api('/projects/owned', user4)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include('statistics')
+ end
+
+ it "includes statistics if requested" do
+ attributes = {
+ commit_count: 23,
+ storage_size: 702,
+ repository_size: 123,
+ lfs_objects_size: 234,
+ build_artifacts_size: 345,
+ }
+
+ project4.statistics.update!(attributes)
+
+ get v3_api('/projects/owned', user4), statistics: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['statistics']).to eq attributes.stringify_keys
+ end
+ end
+ end
+
+ describe 'GET /projects/visible' do
+ shared_examples_for 'visible projects response' do
+ it 'returns the visible projects' do
+ get v3_api('/projects/visible', current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id))
+ end
+ end
+
+ let!(:public_project) { create(:empty_project, :public) }
+ before do
+ project
+ project2
+ project3
+ project4
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { nil }
+ let(:projects) { [public_project] }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user }
+ let(:projects) { [public_project, project, project2, project3] }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'visible projects response' do
+ let(:current_user) { user2 }
+ let(:projects) { [public_project] }
+ end
+ end
+ end
+
+ describe 'GET /projects/starred' do
+ let(:public_project) { create(:empty_project, :public) }
+
+ before do
+ project_member2
+ user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ end
+
+ it 'returns the starred projects viewable by the user' do
+ get v3_api('/projects/starred', user3)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id)
+ end
+ end
+
+ describe 'POST /projects' do
+ context 'maximum number of projects reached' do
+ it 'does not create new project and respond with 403' do
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
+ expect { post v3_api('/projects', user2), name: 'foo' }.
+ to change {Project.count}.by(0)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ it 'creates new project without path and return 201' do
+ expect { post v3_api('/projects', user), name: 'foo' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+ end
+
+ it 'creates last project before reaching project limit' do
+ allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
+ post v3_api('/projects', user2), name: 'foo'
+ expect(response).to have_http_status(201)
+ end
+
+ it 'does not create new project without name and return 400' do
+ expect { post v3_api('/projects', user) }.not_to change { Project.count }
+ expect(response).to have_http_status(400)
+ end
+
+ it "assigns attributes to project" do
+ project = attributes_for(:project, {
+ path: 'camelCasePath',
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false,
+ request_access_enabled: true,
+ only_allow_merge_if_all_discussions_are_resolved: false
+ })
+
+ post v3_api('/projects', user), project
+
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+
+ # Check feature permissions attributes
+ project = Project.find_by_path(project[:path])
+ expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api('/projects', user), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post v3_api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post v3_api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_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 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 v3_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 })
+
+ post v3_api('/projects', user), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+
+ context 'when a visibility level is restricted' do
+ before do
+ @project = attributes_for(:project, { public: true })
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it 'does not allow a non-admin to use a restricted visibility level' do
+ post v3_api('/projects', user), @project
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['visibility_level'].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
+
+ it 'allows an admin to override restricted visibility settings' do
+ post v3_api('/projects', admin), @project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to(
+ eq(Gitlab::VisibilityLevel::PUBLIC)
+ )
+ end
+ end
+ end
+
+ describe 'POST /projects/user/:id' do
+ before { project }
+ before { admin }
+
+ it 'should create new project without path and return 201' do
+ expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
+ expect(response).to have_http_status(201)
+ end
+
+ it 'responds with 400 on failure and not project' do
+ expect { post v3_api("/projects/user/#{user.id}", admin) }.
+ not_to change { Project.count }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'assigns attributes to project' do
+ project = attributes_for(:project, {
+ description: FFaker::Lorem.sentence,
+ issues_enabled: false,
+ merge_requests_enabled: false,
+ wiki_enabled: false,
+ request_access_enabled: true
+ })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ project.each_pair do |k, v|
+ next if %i[has_external_issue_tracker path].include?(k)
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'sets a project as public' do
+ project = attributes_for(:project, :public)
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as public using :public' do
+ project = attributes_for(:project, { public: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'sets a project as internal' do
+ project = attributes_for(:project, :internal)
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as internal overriding :public' do
+ project = attributes_for(:project, :internal, { public: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(response).to have_http_status(201)
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'sets a project as private' do
+ project = attributes_for(:project, :private)
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as private using :public' do
+ project = attributes_for(:project, { public: false })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['public']).to be_falsey
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post v3_api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
+ it 'sets a project as allowing merge even if discussions are unresolved' do
+ project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+
+ post v3_api("/projects/user/#{user.id}", admin), 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 })
+
+ post v3_api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
+ end
+ end
+
+ describe "POST /projects/:id/uploads" do
+ before { project }
+
+ it "uploads the file and returns its info" do
+ post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+
+ expect(response).to have_http_status(201)
+ expect(json_response['alt']).to eq("dk")
+ expect(json_response['url']).to start_with("/uploads/")
+ expect(json_response['url']).to end_with("/dk.png")
+ end
+ end
+
+ describe 'GET /projects/:id' do
+ context 'when unauthenticated' do
+ it 'returns the public projects' do
+ public_project = create(:empty_project, :public)
+
+ get v3_api("/projects/#{public_project.id}")
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(public_project.id)
+ expect(json_response['description']).to eq(public_project.description)
+ expect(json_response.keys).not_to include('permissions')
+ end
+ end
+
+ context 'when authenticated' do
+ before do
+ project
+ project_member
+ end
+
+ it 'returns a project by id' do
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['public']).to be_falsey
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility_level']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['builds_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_builds']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ end
+
+ it 'returns a project by path name' do
+ get v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42', user)
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+ get v3_api("/projects/#{project.id}", other_user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'handles users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
+ it 'exposes namespace fields' do
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['namespace']).to eq({
+ 'id' => user.namespace.id,
+ 'name' => user.namespace.name,
+ 'path' => user.namespace.path,
+ 'kind' => user.namespace.kind,
+ })
+ end
+
+ describe 'permissions' do
+ context 'all projects' do
+ before { project.team << [user, :master] }
+
+ it 'contains permission information' do
+ get v3_api("/projects", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.first['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response.first['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'personal project' do
+ it 'sets project access and returns 200' do
+ project.team << [user, :master]
+ get v3_api("/projects/#{project.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']['access_level']).
+ to eq(Gitlab::Access::MASTER)
+ expect(json_response['permissions']['group_access']).to be_nil
+ end
+ end
+
+ context 'group project' do
+ let(:project2) { create(:empty_project, group: create(:group)) }
+
+ before { project2.group.add_owner(user) }
+
+ it 'sets the owner and return 200' do
+ get v3_api("/projects/#{project2.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['permissions']['project_access']).to be_nil
+ expect(json_response['permissions']['group_access']['access_level']).
+ to eq(Gitlab::Access::OWNER)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/events' do
+ shared_examples_for 'project events response' do
+ it 'returns the project events' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+ note = create(:note_on_issue, note: 'What an awesome day!', project: project)
+ EventCreateService.new.leave_note(note, note.author)
+
+ get v3_api("/projects/#{project.id}/events", current_user)
+
+ expect(response).to have_http_status(200)
+
+ first_event = json_response.first
+
+ expect(first_event['action_name']).to eq('commented on')
+ expect(first_event['note']['body']).to eq('What an awesome day!')
+
+ last_event = json_response.last
+
+ expect(last_event['action_name']).to eq('joined')
+ expect(last_event['project_id'].to_i).to eq(project.id)
+ expect(last_event['author_username']).to eq(member.username)
+ expect(last_event['author']['name']).to eq(member.name)
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project events response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project events response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/events", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/users' do
+ shared_examples_for 'project users response' do
+ it 'returns the project users' do
+ member = create(:user)
+ create(:project_member, :developer, user: member, project: project)
+
+ get v3_api("/projects/#{project.id}/users", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+
+ first_user = json_response.first
+
+ expect(first_user['username']).to eq(member.username)
+ expect(first_user['name']).to eq(member.name)
+ expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project users response' do
+ let(:project) { create(:empty_project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ context 'valid request' do
+ it_behaves_like 'project users response' do
+ let(:current_user) { user }
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get v3_api('/projects/42/users', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+
+ it 'returns a 404 error if user is not a member' do
+ other_user = create(:user)
+
+ get v3_api("/projects/#{project.id}/users", other_user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/snippets' do
+ before { snippet }
+
+ it 'returns an array of project snippets' do
+ get v3_api("/projects/#{project.id}/snippets", user)
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(snippet.title)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id' do
+ it 'returns a project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(snippet.title)
+ end
+
+ it 'returns a 404 error if snippet id not found' do
+ get v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/snippets' do
+ it 'creates a new project snippet' do
+ post v3_api("/projects/#{project.id}/snippets", user),
+ title: 'v3_api test', file_name: 'sample.rb', code: 'test',
+ visibility_level: '0'
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('v3_api test')
+ end
+
+ it 'returns a 400 error if invalid snippet is given' do
+ post v3_api("/projects/#{project.id}/snippets", user)
+ expect(status).to eq(400)
+ end
+ end
+
+ describe 'PUT /projects/:id/snippets/:snippet_id' do
+ it 'updates an existing project snippet' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ code: 'updated code'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('example')
+ expect(snippet.reload.content).to eq('updated code')
+ end
+
+ it 'updates an existing project snippet with new title' do
+ put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user),
+ title: 'other v3_api test'
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq('other v3_api test')
+ end
+ end
+
+ describe 'DELETE /projects/:id/snippets/:snippet_id' do
+ before { snippet }
+
+ it 'deletes existing project snippet' do
+ expect do
+ delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user)
+ end.to change { Snippet.count }.by(-1)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns 404 when deleting unknown snippet id' do
+ delete v3_api("/projects/#{project.id}/snippets/1234", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/snippets/:snippet_id/raw' do
+ it 'gets a raw project snippet' do
+ get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 error if raw project snippet not found' do
+ get v3_api("/projects/#{project.id}/snippets/5555/raw", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe :fork_admin do
+ let(:project_fork_target) { create(:empty_project) }
+ let(:project_fork_source) { create(:empty_project, :public) }
+
+ describe 'POST /projects/:id/fork/:forked_from_id' do
+ let(:new_project_fork_source) { create(:empty_project, :public) }
+
+ it "is not available for non admin users" do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'allows project to be forked from an existing project' do
+ expect(project_fork_target.forked?).not_to be_truthy
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ expect(response).to have_http_status(201)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked_project_link).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ end
+
+ it 'fails if forked_from project which does not exist' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'fails with 409 if already forked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin)
+ expect(response).to have_http_status(409)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
+ expect(project_fork_target.forked?).to be_truthy
+ end
+ end
+
+ describe 'DELETE /projects/:id/fork' do
+ it "is not visible to users outside group" do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user)
+ expect(response).to have_http_status(404)
+ end
+
+ context 'when users belong to project group' do
+ let(:project_fork_target) { create(:empty_project, group: create(:group)) }
+
+ before do
+ project_fork_target.group.add_owner user
+ project_fork_target.group.add_developer user2
+ end
+
+ it 'is forbidden to non-owner users' do
+ delete v3_api("/projects/#{project_fork_target.id}/fork", user2)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'makes forked project unforked' do
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(200)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).to be_nil
+ expect(project_fork_target.forked?).not_to be_truthy
+ end
+
+ it 'is idempotent if not forked' do
+ expect(project_fork_target.forked_from_project).to be_nil
+ delete v3_api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(304)
+ expect(project_fork_target.reload.forked_from_project).to be_nil
+ end
+ end
+ end
+ end
+
+ describe "POST /projects/:id/share" do
+ let(:group) { create(:group) }
+
+ it "shares project with group" do
+ expires_at = 10.days.from_now.to_date
+
+ expect do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
+ end.to change { ProjectGroupLink.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['group_id']).to eq(group.id)
+ expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+ expect(json_response['expires_at']).to eq(expires_at.to_s)
+ end
+
+ it "returns a 400 error when group id is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when access level is not given" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns a 400 error when sharing is disabled" do
+ project.namespace.update(share_with_group_lock: true)
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when user cannot read group' do
+ private_group = create(:group, :private)
+
+ post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when group does not exist' do
+ post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns a 400 error when wrong params passed" do
+ post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq 'group_access does not have a valid value'
+ end
+ end
+
+ describe 'DELETE /projects/:id/share/:group_id' do
+ it 'returns 204 when deleting a group share' do
+ group = create(:group, :public)
+ create(:project_group_link, group: group, project: project)
+
+ delete v3_api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_http_status(204)
+ expect(project.project_group_links).to be_empty
+ end
+
+ it 'returns a 400 when group id is not an integer' do
+ delete v3_api("/projects/#{project.id}/share/foo", user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 404 error when group link does not exist' do
+ delete v3_api("/projects/#{project.id}/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 404 error when project does not exist' do
+ delete v3_api("/projects/123/share/1234", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/search/:query' do
+ let!(:query) { 'query'}
+ let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) }
+ let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) }
+ let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) }
+ let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) }
+ let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") }
+ let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
+ let!(:public) { create(:empty_project, :public, name: "public #{query}") }
+ let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
+ let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
+
+ shared_examples_for 'project search response' do |args = {}|
+ it 'returns project search responses' do
+ get v3_api("/projects/search/#{args[:query]}", current_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(args[:results])
+ json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
+ end
+ end
+
+ context 'when unauthenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 1 do
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when authenticated' do
+ it_behaves_like 'project search response', query: 'query', results: 6 do
+ let(:current_user) { user }
+ end
+ it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated as a different user' do
+ it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
+ let(:current_user) { user2 }
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id' do
+ before { project }
+ before { user }
+ before { user3 }
+ before { user4 }
+ before { project3 }
+ before { project4 }
+ before { project_member3 }
+ before { project_member2 }
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.id}"), project_param
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated as project owner' do
+ it 'updates name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates visibility_level from public to private' do
+ project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project_param = { public: false }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'does not update name to existing name' do
+ project_param = { name: project3.name }
+ put v3_api("/projects/#{project.id}", user), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['name']).to eq(['has already been taken'])
+ end
+
+ it 'updates request_access_enabled' do
+ project_param = { request_access_enabled: false }
+
+ put v3_api("/projects/#{project.id}", user), project_param
+
+ expect(response).to have_http_status(200)
+ expect(json_response['request_access_enabled']).to eq(false)
+ end
+
+ it 'updates path & name to existing path & name in different namespace' do
+ project_param = { path: project4.path, name: project4.name }
+ put v3_api("/projects/#{project3.id}", user), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+ end
+
+ context 'when authenticated as project master' do
+ it 'updates path' do
+ project_param = { path: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'updates other attributes' do
+ project_param = { issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description' }
+
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ end
+
+ it 'does not update path to existing path' do
+ project_param = { path: project.path }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['path']).to eq(['has already been taken'])
+ end
+
+ it 'does not update name' do
+ project_param = { name: 'bar' }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not update visibility_level' do
+ project_param = { visibility_level: 20 }
+ put v3_api("/projects/#{project3.id}", user4), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when authenticated as project developer' do
+ it 'does not update other attributes' do
+ project_param = { path: 'bar',
+ issues_enabled: true,
+ wiki_enabled: true,
+ snippets_enabled: true,
+ merge_requests_enabled: true,
+ description: 'new description',
+ request_access_enabled: true }
+ put v3_api("/projects/#{project.id}", user3), project_param
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/archive' do
+ context 'on an unarchived project' do
+ it 'archives the project' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'remains archived' do
+ post v3_api("/projects/#{project.id}/archive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_truthy
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/archive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/unarchive' do
+ context 'on an unarchived project' do
+ it 'remains unarchived' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'on an archived project' do
+ before do
+ project.archive!
+ end
+
+ it 'unarchives the project' do
+ post v3_api("/projects/#{project.id}/unarchive", user)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['archived']).to be_falsey
+ end
+ end
+
+ context 'user without archiving rights to the project' do
+ before do
+ project.team << [user3, :developer]
+ end
+
+ it 'rejects the action' do
+ post v3_api("/projects/#{project.id}/unarchive", user3)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/star' do
+ context 'on an unstarred project' do
+ it 'stars the project' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['star_count']).to eq(1)
+ end
+ end
+
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'does not modify the star count' do
+ expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/star' do
+ context 'on a starred project' do
+ before do
+ user.toggle_star(project)
+ project.reload
+ end
+
+ it 'unstars the project' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['star_count']).to eq(0)
+ end
+ end
+
+ context 'on an unstarred project' do
+ it 'does not modify the star count' do
+ expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count }
+
+ expect(response).to have_http_status(304)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id' do
+ context 'when authenticated as user' do
+ it 'removes project' do
+ delete v3_api("/projects/#{project.id}", user)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a project if not an owner' do
+ user3 = create(:user)
+ project.team << [user3, :developer]
+ delete v3_api("/projects/#{project.id}", user3)
+ expect(response).to have_http_status(403)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', user)
+ expect(response).to have_http_status(404)
+ end
+
+ it 'does not remove a project not attached to user' do
+ delete v3_api("/projects/#{project.id}", user2)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'removes any existing project' do
+ delete v3_api("/projects/#{project.id}", admin)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'does not remove a non existing project' do
+ delete v3_api('/projects/1328', admin)
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 8dbe5f0b025..d85afdeab42 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -288,7 +288,7 @@ describe Ci::API::Builds do
expect(build.reload.trace).to eq 'BUILD TRACE'
end
- context 'build has been erased' do
+ context 'job has been erased' do
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
it 'responds with forbidden' do
@@ -458,7 +458,7 @@ describe Ci::API::Builds do
before { build.run! }
describe "POST /builds/:id/artifacts/authorize" do
- context "should authorize posting artifact to running build" do
+ context "authorizes posting artifact to running build" do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
@@ -492,7 +492,7 @@ describe Ci::API::Builds do
end
end
- context "should fail to post too large artifact" do
+ context "fails to post too large artifact" do
it "using token as parameter" do
stub_application_setting(max_artifacts_size: 0)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 4a16824de04..87786e85621 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -57,7 +57,7 @@ describe 'Git HTTP requests', lib: true do
end
context 'but the repo is disabled' do
- let(:project) { create(:project, repository_access_level: ProjectFeature::DISABLED, wiki_access_level: ProjectFeature::ENABLED) }
+ let(:project) { create(:project, :repository_disabled, :wiki_enabled) }
let(:wiki) { ProjectWiki.new(project) }
let(:path) { "/#{wiki.repository.path_with_namespace}.git" }
@@ -141,7 +141,7 @@ describe 'Git HTTP requests', lib: true do
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)
+ project = create(:project, :public, :repository_disabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -151,7 +151,7 @@ describe 'Git HTTP requests', lib: true do
context 'but the repo is enabled' do
it 'allows to clone the repo' do
- project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED)
+ project = create(:project, :public, :repository_enabled)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:ok)
@@ -161,7 +161,7 @@ describe 'Git HTTP requests', lib: true do
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)
+ project = create(:project, :public, :repository_private)
download("#{project.path_with_namespace}.git", {}) do |response|
expect(response).to have_http_status(:unauthorized)
@@ -360,10 +360,6 @@ describe 'Git HTTP requests', lib: true do
let(:project) { build.project }
let(:other_project) { create(:empty_project) }
- before do
- project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
- end
-
context 'when build created by system is authenticated' do
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 9bfc84c7425..c0e7bab8199 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -600,6 +600,7 @@ describe 'Git LFS API and storage' do
expect(json_response).to eq('objects' => [
{ 'oid' => sample_oid,
'size' => sample_size,
+ 'authenticated' => true,
'actions' => {
'download' => {
'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 77549db2927..a5bc62ef6c2 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe 'project routing' do
before do
- allow(Project).to receive(:find_with_namespace).and_return(false)
- allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true)
+ allow(Project).to receive(:find_by_full_path).and_return(false)
+ allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true)
end
# Shared examples for a resource inside a Project
@@ -27,35 +27,42 @@ describe 'project routing' do
# let(:actions) { [:index] }
# let(:controller) { 'issues' }
# end
+ #
+ # # Different controller name and path
+ # it_behaves_like 'RESTful project resources' do
+ # let(:controller) { 'pages_domains' }
+ # let(:controller_path) { 'pages/domains' }
+ # end
shared_examples 'RESTful project resources' do
let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] }
+ let(:controller_path) { controller }
it 'to #index' do
- expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
+ expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index)
end
it 'to #create' do
- expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
+ expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create)
end
it 'to #new' do
- expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new)
end
it 'to #edit' do
- expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit)
end
it 'to #show' do
- expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
+ expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show)
end
it 'to #update' do
- expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
+ expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update)
end
it 'to #destroy' do
- expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
+ expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy)
end
end
@@ -86,13 +93,13 @@ describe 'project routing' do
end
context 'name with dot' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) }
it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') }
end
context 'with nested group' do
- before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) }
+ before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) }
it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') }
end
@@ -539,4 +546,20 @@ describe 'project routing' do
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ describe Projects::PagesDomainsController, 'routing' do
+ it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:show, :new, :create, :destroy] }
+ let(:controller) { 'pages_domains' }
+ let(:controller_path) { 'pages/domains' }
+ end
+
+ it 'to #destroy with a valid domain name' do
+ expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+
+ it 'to #show with a valid domain' do
+ expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com')
+ end
+ end
end
diff --git a/spec/rubocop/cop/gem_fetcher_spec.rb b/spec/rubocop/cop/gem_fetcher_spec.rb
new file mode 100644
index 00000000000..c07f6a831dc
--- /dev/null
+++ b/spec/rubocop/cop/gem_fetcher_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/gem_fetcher'
+
+describe RuboCop::Cop::GemFetcher do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in Gemfile' do
+ before do
+ allow(cop).to receive(:gemfile?).and_return(true)
+ end
+
+ it 'registers an offense when a gem uses `git`' do
+ inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['git: "https://gitlab.com/foo/bar.git"'])
+ end
+ end
+
+ it 'registers an offense when a gem uses `github`' do
+ inspect_source(cop, 'gem "foo", github: "foo/bar.git"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['github: "foo/bar.git"'])
+ end
+ end
+ end
+
+ context 'outside of Gemfile' do
+ it 'registers no offense' do
+ inspect_source(cop, 'gem "foo", git: "https://gitlab.com/foo/bar.git"')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
new file mode 100644
index 00000000000..6b9b6b19650
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_column_with_default'
+
+describe RuboCop::Cop::Migration::AddColumnWithDefault do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when add_column_with_default is used inside a change method' do
+ inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when add_column_with_default is used inside an up method' do
+ inspect_source(cop, 'def up; add_column_with_default :table, :column, default: false; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source(cop, 'def change; add_column_with_default :table, :column, default: false; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
new file mode 100644
index 00000000000..7cb24dc5646
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/migration/add_concurrent_foreign_key'
+
+describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
+ include CopHelper
+
+ let(:cop) { described_class.new }
+
+ context 'outside of a migration' do
+ it 'does not register any offenses' do
+ inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'in a migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when using add_foreign_key' do
+ inspect_source(cop, 'def up; add_foreign_key(:projects, :users, column: :user_id); end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_serializer_spec.rb b/spec/serializers/analytics_build_serializer_spec.rb
index f0551c78671..e3b1dd93dc2 100644
--- a/spec/serializers/analytics_build_serializer_spec.rb
+++ b/spec/serializers/analytics_build_serializer_spec.rb
@@ -1,17 +1,13 @@
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) }
+ subject { described_class.new.represent(resource) }
+
context 'when there is a single object provided' do
it 'contains important elements of analyticsBuild' do
- expect(json)
+ expect(subject)
.to include(:name, :branch, :short_sha, :date, :total_time, :url, :author)
end
end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index 6afbb2df35c..2f08958a783 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -1,14 +1,13 @@
require 'spec_helper'
describe AnalyticsIssueSerializer do
- let(:serializer) do
+ subject 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
{
@@ -23,7 +22,7 @@ describe AnalyticsIssueSerializer do
context 'when there is a single object provided' do
it 'contains important elements of the issue' do
- expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author)
+ expect(subject).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
index cdfae27193f..62067cc0ef2 100644
--- a/spec/serializers/analytics_merge_request_serializer_spec.rb
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -1,14 +1,13 @@
require 'spec_helper'
describe AnalyticsMergeRequestSerializer do
- let(:serializer) do
+ subject 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
{
@@ -24,7 +23,7 @@ describe AnalyticsMergeRequestSerializer do
context 'when there is a single object provided' do
it 'contains important elements of the merge request' do
- expect(json).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
+ expect(subject).to include(:title, :iid, :created_at, :total_time, :url, :author, :state)
end
end
end
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
index f9951826683..be6aa7c65c3 100644
--- a/spec/serializers/analytics_stage_serializer_spec.rb
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe AnalyticsStageSerializer do
- let(:serializer) do
- described_class
- .new.represent(resource)
+ subject do
+ described_class.new.represent(resource)
end
- let(:json) { serializer.as_json }
- let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) }
+ let(:resource) do
+ Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {})
+ end
before do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12)
@@ -15,10 +15,10 @@ describe AnalyticsStageSerializer do
end
it 'it generates payload for single object' do
- expect(json).to be_kind_of Hash
+ expect(subject).to be_kind_of Hash
end
it 'contains important elements of AnalyticsStage' do
- expect(json).to include(:title, :description, :value)
+ expect(subject).to include(:title, :description, :value)
end
end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index 7a84c8b0b40..5d7a94c2d02 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -1,29 +1,28 @@
require 'spec_helper'
describe AnalyticsSummarySerializer do
- let(:serializer) do
- described_class
- .new.represent(resource)
+ subject do
+ described_class.new.represent(resource)
end
- let(:json) { serializer.as_json }
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
+
let(:resource) do
- Gitlab::CycleAnalytics::Summary::Issue.new(project: double,
- from: 1.day.ago,
- current_user: user)
+ Gitlab::CycleAnalytics::Summary::Issue
+ .new(project: double, from: 1.day.ago, current_user: user)
end
before do
- allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12)
+ allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue)
+ .to receive(:value).and_return(1.12)
end
it 'it generates payload for single object' do
- expect(json).to be_kind_of Hash
+ expect(subject).to be_kind_of Hash
end
it 'contains important elements of AnalyticsStage' do
- expect(json).to include(:title, :value)
+ expect(subject).to include(:title, :value)
end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index b7ed4eb0239..1b95f1ff198 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -1,16 +1,15 @@
require 'spec_helper'
describe EnvironmentSerializer do
- let(:serializer) do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:json) do
described_class
.new(user: user, project: project)
.represent(resource)
end
- let(:json) { serializer.as_json }
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
context 'when there is a single object provided' do
before do
create(:ci_build, :manual, name: 'manual1',
@@ -53,4 +52,136 @@ describe EnvironmentSerializer do
expect(json).to be_an_instance_of Array
end
end
+
+ context 'when representing environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project).within_folders
+ end
+
+ let(:resource) { Environment.all }
+
+ subject { serializer.represent(resource) }
+
+ context 'when there is a single environment' do
+ before { create(:environment, name: 'staging') }
+
+ it 'represents one standalone environment' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple environments in folder' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ end
+
+ it 'represents one item that is a folder' do
+ expect(subject.count).to eq 1
+ expect(subject.first[:name]).to eq 'staging'
+ expect(subject.first[:size]).to eq 2
+ expect(subject.first[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.first[:latest][:environment_type]).to eq 'staging'
+ end
+ end
+
+ context 'when there are multiple folders and standalone environments' do
+ before do
+ create(:environment, name: 'staging/my-review-1')
+ create(:environment, name: 'staging/my-review-2')
+ create(:environment, name: 'production/my-review-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'represents multiple items grouped within folders' do
+ expect(subject.count).to eq 3
+
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.first[:size]).to eq 1
+ expect(subject.first[:latest][:name]).to eq 'production/my-review-3'
+ expect(subject.first[:latest][:environment_type]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ expect(subject.second[:size]).to eq 2
+ expect(subject.second[:latest][:name]).to eq 'staging/my-review-2'
+ expect(subject.second[:latest][:environment_type]).to eq 'staging'
+ expect(subject.third[:name]).to eq 'testing'
+ expect(subject.third[:size]).to eq 1
+ expect(subject.third[:latest][:name]).to eq 'testing'
+ expect(subject.third[:latest][:environment_type]).to be_nil
+ end
+ end
+ end
+
+ context 'when used with pagination' do
+ let(:request) { spy('request') }
+ let(:response) { spy('response') }
+ let(:resource) { Environment.all }
+ let(:pagination) { { page: 1, per_page: 2 } }
+
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ end
+
+ before do
+ allow(request).to receive(:query_parameters)
+ .and_return(pagination)
+ end
+
+ subject { serializer.represent(resource) }
+
+ it 'creates a paginated serializer' do
+ expect(serializer).to be_paginated
+ end
+
+ context 'when resource is paginatable relation' do
+ context 'when there is a single environment object in relation' do
+ before { create(:environment) }
+
+ it 'serializes environments' do
+ expect(subject.first).to have_key :id
+ end
+ end
+
+ context 'when multiple environment objects are serialized' do
+ before { create_list(:environment, 3) }
+
+ it 'serializes appropriate number of objects' do
+ expect(subject.count).to be 2
+ end
+
+ it 'appends relevant headers' do
+ expect(response).to receive(:[]=).with('X-Total', '3')
+ expect(response).to receive(:[]=).with('X-Total-Pages', '2')
+ expect(response).to receive(:[]=).with('X-Per-Page', '2')
+
+ subject
+ end
+ end
+
+ context 'when grouping environments within folders' do
+ let(:serializer) do
+ described_class.new(project: project)
+ .with_pagination(request, response)
+ .within_folders
+ end
+
+ before do
+ create(:environment, name: 'staging/review-1')
+ create(:environment, name: 'staging/review-2')
+ create(:environment, name: 'production/deploy-3')
+ create(:environment, name: 'testing')
+ end
+
+ it 'paginates grouped items including ordering' do
+ expect(subject.count).to eq 2
+ expect(subject.first[:name]).to eq 'production'
+ expect(subject.second[:name]).to eq 'staging'
+ end
+ end
+ end
+ end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 3a32cb394dd..2aaef03cb93 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -7,11 +7,7 @@ describe PipelineSerializer do
described_class.new(user: user)
end
- let(:entity) do
- serializer.represent(resource)
- end
-
- subject { entity.as_json }
+ subject { serializer.represent(resource) }
describe '#represent' do
context 'when used without pagination' do
@@ -56,14 +52,14 @@ describe PipelineSerializer do
expect(serializer).to be_paginated
end
- context 'when resource does is not paginatable' do
+ context 'when resource is not paginatable' do
context 'when a single pipeline object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) }
let(:pagination) { { page: 1, per_page: 1 } }
it 'raises error' do
- expect { subject }
- .to raise_error(PipelineSerializer::InvalidResourceError)
+ expect { subject }.to raise_error(
+ Gitlab::Serializer::Pagination::InvalidResourceError)
end
end
end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index fde807cc410..7b29b043296 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do
expect { service.execute }.to change(Board, :count).by(1)
end
- it 'creates default lists' do
+ it 'creates the default lists' do
board = service.execute
- expect(board.lists.size).to eq 2
- expect(board.lists.first).to be_backlog
- expect(board.lists.last).to be_done
+ expect(board.lists.size).to eq 1
+ expect(board.lists.first).to be_done
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 7c206cf3ce7..305278843f5 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -13,7 +13,6 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board) }
@@ -45,8 +44,8 @@ describe Boards::Issues::ListService, services: true do
end
context 'sets default order to priority' do
- it 'returns opened issues when listing issues from Backlog' do
- params = { board_id: board.id, id: backlog.id }
+ it 'returns opened issues when list id is missing' do
+ params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index c43b2aec490..77f75167b3d 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { create(:backlog_list, board: board1) }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:done) { create(:done_list, board: board1) }
@@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do
project.team << [user, :developer]
end
- context 'when moving from backlog' do
- it 'adds the label of the list it goes to' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug, development)
- end
- end
-
- context 'when moving to backlog' do
- it 'removes all list-labels' do
- issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
- params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
-
- expect(issue.reload.labels).to contain_exactly(bug)
- end
- end
-
- context 'when moving from backlog to done' do
- it 'closes the issue' do
- issue = create(:labeled_issue, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_closed
- end
- end
-
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
@@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do
end
end
- context 'when moving from done to backlog' do
- it 'reopens the issue' do
- issue = create(:labeled_issue, :closed, project: project, labels: [bug])
- params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id }
-
- described_class.new(project, user, params).execute(issue)
- issue.reload
-
- expect(issue.labels).to contain_exactly(bug)
- expect(issue).to be_reopened
- end
- end
-
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index a7e9efcf93f..ebac38e68f1 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, and done lists' do
+ context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
@@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has backlog, label and done lists' do
+ context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 628caf03476..a30860f828a 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
@@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do
described_class.new(project, user).execute(development)
- expect(backlog.reload.position).to be_nil
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(done.reload.position).to be_nil
end
end
- it 'does not remove list from board when list type is backlog' do
- list = board.backlog_list
- service = described_class.new(project, user)
-
- expect { service.execute(list) }.not_to change(board.lists, :count)
- end
-
it 'does not remove list from board when list type is done' do
list = board.done_list
service = described_class.new(project, user)
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 334cee3f06d..2dffc62b215 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double)
- expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list]
+ expect(service.execute(board)).to eq [list, board.done_list]
end
end
end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 63fa0bb8c5f..3786dc82bf0 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
- let!(:backlog) { create(:backlog_list, board: board) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
@@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do
end
end
- it 'keeps position of lists when list type is backlog' do
- service = described_class.new(project, user, position: 2)
-
- service.execute(backlog)
-
- expect(current_list_positions).to eq [0, 1, 2, 3]
- end
-
it 'keeps position of lists when list type is done' do
service = described_class.new(project, user, position: 2)
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 6f7d1a5d28d..560f83d94f7 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -42,10 +42,10 @@ describe Ci::StopEnvironmentsService, services: true do
end
end
- context 'when environment is not stoppable' do
+ context 'when environment is not stopped' do
before do
allow_any_instance_of(Environment)
- .to receive(:stoppable?).and_return(false)
+ .to receive(:state).and_return(:stopped)
end
it 'does not stop environment' do
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 3760f19aaa2..0a7fc58523f 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -3,17 +3,17 @@ require 'spec_helper'
describe CompareService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:service) { described_class.new }
+ let(:service) { described_class.new(project, 'feature') }
describe '#execute' do
context 'compare with base, like feature...fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: false) }
+ subject { service.execute(project, 'fix', straight: false) }
it { expect(subject.diffs.size).to eq(1) }
end
context 'straight compare, like feature..fix' do
- subject { service.execute(project, 'feature', project, 'fix', straight: true) }
+ subject { service.execute(project, 'fix', straight: true) }
it { expect(subject.diffs.size).to eq(3) }
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index b7dc99ed887..f2c2009bcbf 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -9,7 +9,7 @@ describe EventCreateService, services: true do
it { expect(service.open_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -19,7 +19,7 @@ describe EventCreateService, services: true do
it { expect(service.close_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_issue(issue, issue.author) }.to change { Event.count }
end
end
@@ -29,7 +29,7 @@ describe EventCreateService, services: true do
it { expect(service.reopen_issue(issue, issue.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.reopen_issue(issue, issue.author) }.to change { Event.count }
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d3c37c7820f..35e6e139238 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -6,7 +6,10 @@ describe Files::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:file_path) { 'files/ruby/popen.rb' }
- let(:new_contents) { "New Content" }
+ let(:new_contents) { 'New Content' }
+ let(:target_branch) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
let(:commit_params) do
{
file_path: file_path,
@@ -14,9 +17,9 @@ describe Files::UpdateService do
file_content: new_contents,
file_content_encoding: "text",
last_commit_sha: last_commit_sha,
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
+ start_project: project,
+ start_branch: project.default_branch,
+ target_branch: target_branch
}
end
@@ -54,18 +57,6 @@ describe Files::UpdateService do
end
context "when the last_commit_sha is not supplied" do
- let(:commit_params) do
- {
- file_path: file_path,
- commit_message: "Update File",
- file_content: new_contents,
- file_content_encoding: "text",
- source_project: project,
- source_branch: project.default_branch,
- target_branch: project.default_branch,
- }
- end
-
it "returns a hash with the :success status " do
results = subject.execute
@@ -80,5 +71,15 @@ describe Files::UpdateService do
expect(results.data).to eq(new_contents)
end
end
+
+ context 'when target branch is different than source branch' do
+ let(:target_branch) { "#{project.default_branch}-new" }
+
+ it 'fires hooks only once' do
+ expect(GitHooksService).to receive(:new).once.and_call_original
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 41b0968b8b4..3318dfb22b6 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -21,7 +21,7 @@ describe GitHooksService, services: true do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
- expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil])
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }
end
end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 538e85cdc89..f86189b68e9 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
-describe DestroyGroupService, services: true do
+describe Groups::DestroyService, services: true do
include DatabaseConnectionHelpers
- let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:project) { create(:project, namespace: group) }
+ let!(:user) { create(:user) }
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new }
- let!(:remove_path) { group.path + "+#{group.id}+deleted" }
+ let!(:remove_path) { group.path + "+#{group.id}+deleted" }
shared_examples 'group destruction' do |async|
context 'database records' do
@@ -43,9 +43,9 @@ describe DestroyGroupService, services: true do
def destroy_group(group, user, async)
if async
- DestroyGroupService.new(group, user).async_execute
+ Groups::DestroyService.new(group, user).async_execute
else
- DestroyGroupService.new(group, user).execute
+ Groups::DestroyService.new(group, user).execute
end
end
end
@@ -80,7 +80,7 @@ describe DestroyGroupService, services: true do
# 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)
+ Thread.new { Groups::DestroyService.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
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 531180e48a1..7c0fccb9d41 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -51,7 +51,7 @@ describe Groups::UpdateService, services: true do
end
context 'rename group' do
- let!(:service) { described_class.new(internal_group, user, path: 'new_path') }
+ let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 4cfba35c830..09807e5d35b 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -120,11 +120,20 @@ describe Issues::BuildService, services: true do
end
describe '#execute' do
+ let(:milestone) { create(:milestone, project: project) }
+
it 'builds a new issues with given params' do
- issue = described_class.new(project, user, title: 'Issue #1', description: 'Issue description').execute
+ issue = described_class.new(
+ project,
+ user,
+ title: 'Issue #1',
+ description: 'Issue description',
+ milestone_id: milestone.id,
+ ).execute
expect(issue.title).to eq('Issue #1')
expect(issue.description).to eq('Issue description')
+ expect(issue.milestone).to eq(milestone)
end
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index ac3834c32ff..e1feeed8a67 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -46,6 +46,7 @@ describe Issues::CreateService, services: true do
expect(issue).to be_persisted
expect(issue.title).to eq('Awesome issue')
+ expect(issue.description).to eq('please fix')
expect(issue.assignee).to be_nil
expect(issue.labels).to be_empty
expect(issue.milestone).to be_nil
@@ -181,5 +182,107 @@ describe Issues::CreateService, services: true do
expect(issue.title).to be_nil
end
end
+
+ context 'checking spam' do
+ let(:opts) do
+ {
+ title: 'Awesome issue',
+ description: 'please fix',
+ request: double(:request, env: {})
+ }
+ end
+
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ end
+
+ context 'when recaptcha was verified' do
+ let(:log_user) { user }
+ let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: 'Awesome issue') }
+
+ before do
+ opts[:recaptcha_verified] = true
+ opts[:spam_log_id] = spam_logs.last.id
+
+ expect(AkismetService).not_to receive(:new)
+ end
+
+ it 'does no mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+
+ it 'marks related spam_log as recaptcha_verified' do
+ expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true)
+ end
+
+ context 'when spam log does not belong to a user' do
+ let(:log_user) { create(:user) }
+
+ it 'does not mark spam_log as recaptcha_verified' do
+ expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ end
+ end
+
+ context 'when spam log title does not match the issue title' do
+ before do
+ opts[:title] = 'Another issue'
+ end
+
+ it 'does not mark spam_log as recaptcha_verified' do
+ expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ end
+ end
+ end
+
+ context 'when recaptcha was not verified' do
+ context 'when akismet detects spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ it 'marks an issue as a spam ' do
+ expect(issue).to be_spam
+ end
+
+ it 'an issue is not valid ' do
+ expect(issue.valid?).to be_falsey
+ end
+
+ it 'creates a new spam_log' do
+ expect{issue}.to change{SpamLog.count}.from(0).to(1)
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(issue.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ context 'when akismet does not detect spam' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ end
+
+ it 'does not mark an issue as a spam ' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'an issue is valid ' do
+ expect(issue.valid?).to be_truthy
+ end
+
+ it 'does not assign a spam_log to an issue' do
+ expect(issue.spam_log).to be_nil
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
new file mode 100644
index 00000000000..4b90ad19640
--- /dev/null
+++ b/spec/services/labels/promote_service_spec.rb
@@ -0,0 +1,187 @@
+require 'spec_helper'
+
+describe Labels::PromoteService, services: true do
+ describe '#execute' do
+ let!(:user) { create(:user) }
+
+ context 'project without group' do
+ let!(:project_1) { create(:empty_project) }
+
+ let!(:project_label_1_1) { create(:label, project: project_1) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on project without group' do
+ expect(service.execute(project_label_1_1)).to be_falsey
+ end
+ end
+
+ context 'project with group' do
+ let!(:promoted_label_name) { "Promoted Label" }
+ let!(:untouched_label_name) { "Untouched Label" }
+ let!(:promoted_description) { "Promoted Description" }
+ let!(:promoted_color) { "#0000FF" }
+ let!(:label_2_1_priority) { 1 }
+ let!(:label_3_1_priority) { 2 }
+
+ let!(:group_1) { create(:group) }
+ let!(:group_2) { create(:group) }
+
+ let!(:project_1) { create(:empty_project, namespace: group_1) }
+ let!(:project_2) { create(:empty_project, namespace: group_1) }
+ let!(:project_3) { create(:empty_project, namespace: group_1) }
+ let!(:project_4) { create(:empty_project, namespace: group_2) }
+
+ # Labels/issues can't be lazily created so we might as well eager initialize
+ # all other objects too since we use them inside
+ let!(:project_label_1_1) { create(:label, project: project_1, name: promoted_label_name, color: promoted_color, description: promoted_description) }
+ let!(:project_label_1_2) { create(:label, project: project_1, name: untouched_label_name) }
+ let!(:project_label_2_1) { create(:label, project: project_2, priority: label_2_1_priority, name: promoted_label_name, color: "#FF0000") }
+ let!(:project_label_3_1) { create(:label, project: project_3, priority: label_3_1_priority, name: promoted_label_name) }
+ let!(:project_label_3_2) { create(:label, project: project_3, priority: 1, name: untouched_label_name) }
+ let!(:project_label_4_1) { create(:label, project: project_4, name: promoted_label_name) }
+
+ let!(:issue_1_1) { create(:labeled_issue, project: project_1, labels: [project_label_1_1, project_label_1_2]) }
+ let!(:issue_1_2) { create(:labeled_issue, project: project_1, labels: [project_label_1_2]) }
+ let!(:issue_2_1) { create(:labeled_issue, project: project_2, labels: [project_label_2_1]) }
+ let!(:issue_4_1) { create(:labeled_issue, project: project_4, labels: [project_label_4_1]) }
+
+ let!(:merge_3_1) { create(:labeled_merge_request, source_project: project_3, target_project: project_3, labels: [project_label_3_1, project_label_3_2]) }
+
+ let!(:issue_board_2_1) { create(:board, project: project_2) }
+ let!(:issue_board_list_2_1) { create(:list, board: issue_board_2_1, label: project_label_2_1) }
+
+ let(:new_label) { group_1.labels.find_by(title: promoted_label_name) }
+
+ subject(:service) { described_class.new(project_1, user) }
+
+ it 'fails on group label' do
+ group_label = create(:group_label, group: group_1)
+
+ expect(service.execute(group_label)).to be_falsey
+ end
+
+ it 'is truthy on success' do
+ expect(service.execute(project_label_1_1)).to be_truthy
+ end
+
+ it 'recreates the label as a group label' do
+ expect { service.execute(project_label_1_1) }.
+ to change(project_1.labels, :count).by(-1).
+ and change(group_1.labels, :count).by(1)
+ expect(new_label).not_to be_nil
+ end
+
+ it 'copies title, description and color' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.title).to eq(promoted_label_name)
+ expect(new_label.description).to eq(promoted_description)
+ expect(new_label.color).to eq(promoted_color)
+ end
+
+ it 'merges labels with the same name in group' do
+ expect { service.execute(project_label_1_1) }.to change(project_2.labels, :count).by(-1).and \
+ change(project_3.labels, :count).by(-1)
+ end
+
+ it 'recreates priorities' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_1)).to be_nil
+ expect(new_label.priority(project_2)).to eq(label_2_1_priority)
+ expect(new_label.priority(project_3)).to eq(label_3_1_priority)
+ end
+
+ it 'does not touch project out of promoted group' do
+ service.execute(project_label_1_1)
+ project_4_new_label = project_4.labels.find_by(title: promoted_label_name)
+
+ expect(project_4_new_label).not_to be_nil
+ expect(project_4_new_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'does not touch out of group priority' do
+ service.execute(project_label_1_1)
+
+ expect(new_label.priority(project_4)).to be_nil
+ end
+
+ it 'relinks issue with the promoted label' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not remove untouched labels from issue' do
+ expect { service.execute(project_label_1_1) }.not_to change(issue_1_1.labels, :count)
+ end
+
+ it 'does not relink untouched label in issue' do
+ service.execute(project_label_1_1)
+ issue_label = issue_1_2.labels.find_by(title: untouched_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_1_2.id)
+ end
+
+ it 'relinks issues with merged labels' do
+ service.execute(project_label_1_1)
+ issue_label = issue_2_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(new_label.id)
+ end
+
+ it 'does not relink issues from other group' do
+ service.execute(project_label_1_1)
+ issue_label = issue_4_1.labels.find_by(title: promoted_label_name)
+
+ expect(issue_label).not_to be_nil
+ expect(issue_label.id).to eq(project_label_4_1.id)
+ end
+
+ it 'updates merge request' do
+ service.execute(project_label_1_1)
+ merge_label = merge_3_1.labels.find_by(title: promoted_label_name)
+
+ expect(merge_label).not_to be_nil
+ expect(merge_label.id).to eq(new_label.id)
+ end
+
+ it 'updates board lists' do
+ service.execute(project_label_1_1)
+ list = issue_board_2_1.lists.find_by(label: new_label)
+
+ expect(list).not_to be_nil
+ end
+
+ # In case someone adds a new relation to Label.rb and forgets to relink it
+ # and the database doesn't have foreign key constraints
+ it 'relinks all relations' do
+ service.execute(project_label_1_1)
+
+ Label.reflect_on_all_associations.each do |association|
+ expect(project_label_1_1.send(association.name).any?).to be_falsey
+ end
+ end
+
+ context 'with invalid group label' do
+ before do
+ allow(service).to receive(:clone_label_to_group_label).and_wrap_original do |m, *args|
+ label = m.call(*args)
+ allow(label).to receive(:valid?).and_return(false)
+
+ label
+ end
+ end
+
+ it 'raises an exception' do
+ expect { service.execute(project_label_1_1) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 5f6a7716beb..d55a7657c0e 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do
it { expect(@merge_request).to be_valid }
it { expect(@merge_request).to be_closed }
- it 'should execute hooks with close action' do
+ it 'executes hooks with close action' do
expect(service).to have_received(:execute_hooks).
with(@merge_request, 'close')
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 314ea670a71..983dac6efdb 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -89,7 +89,7 @@ describe MergeRequests::RefreshService, services: true do
# Merge master -> feature branch
author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
@@ -287,41 +287,64 @@ describe MergeRequests::RefreshService, services: true do
it 'references the commit that caused the Work in Progress status' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
-
allow(refresh_service).to receive(:find_new_commits)
refresh_service.instance_variable_set("@commits", [
- instance_double(
- Commit,
+ double(
id: 'aaaaaaa',
+ sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
short_id: 'aaaaaaa',
title: 'Fix issue',
work_in_progress?: false
),
- instance_double(
- Commit,
+ double(
id: 'bbbbbbb',
+ sha: '498214de67004b1da3d820901307bed2a68a8ef6',
short_id: 'bbbbbbb',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'bbbbbbb'
),
- instance_double(
- Commit,
+ double(
id: 'ccccccc',
+ sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141',
short_id: 'ccccccc',
title: 'fixup! Fix issue',
work_in_progress?: true,
to_reference: 'ccccccc'
),
])
-
refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
reload_mrs
-
expect(@merge_request.notes.last.note).to eq(
"marked as a **Work In Progress** from bbbbbbb"
)
end
+
+ it 'does not mark as WIP based on commits that do not belong to an MR' do
+ allow(refresh_service).to receive(:find_new_commits)
+ refresh_service.instance_variable_set("@commits", [
+ double(
+ id: 'aaaaaaa',
+ sha: 'aaaaaaa',
+ short_id: 'aaaaaaa',
+ title: 'Fix issue',
+ work_in_progress?: false
+ ),
+ double(
+ id: 'bbbbbbb',
+ sha: 'bbbbbbbb',
+ short_id: 'bbbbbbb',
+ title: 'fixup! Fix issue',
+ work_in_progress?: true,
+ to_reference: 'bbbbbbb'
+ )
+ ])
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+
+ expect(@merge_request.work_in_progress?).to be_falsey
+ end
end
def reload_mrs
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index 388abb6a0df..a0e51681725 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -66,7 +66,13 @@ describe MergeRequests::ResolveService do
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
- project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+ project.repository.commit_file(
+ user,
+ 'new-file-in-target',
+ '',
+ message: 'Add new file in target',
+ branch_name: 'conflict-start',
+ update: false)
end
before do
diff --git a/spec/services/notes/delete_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index 1d0a747a480..f53f96e0c2b 100644
--- a/spec/services/notes/delete_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Notes::DeleteService, services: true do
+describe Notes::DestroyService, services: true do
describe '#execute' do
it 'deletes a note' do
project = create(:empty_project)
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 309985a5d90..7cf2cd9968f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -443,6 +443,8 @@ describe NotificationService, services: true do
before do
build_team(issue.project)
+ build_group(issue.project)
+
add_users_with_subscription(issue.project, issue)
reset_delivered_emails!
update_custom_notification(:new_issue, @u_guest_custom, project)
@@ -459,6 +461,8 @@ describe NotificationService, services: true do
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
+ should_email(@g_global_watcher)
+ should_email(@g_watcher)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -1218,6 +1222,22 @@ describe NotificationService, services: true do
project.add_master(@u_custom_global)
end
+ # Users in the project's group but not part of project's team
+ # with different notification settings
+ def build_group(project)
+ group = create(:group, :public)
+ project.group = group
+
+ # Group member: global=disabled, group=watch
+ @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group)
+ @g_watcher.notification_settings_for(nil).disabled!
+
+ # Group member: global=watch, group=global
+ @g_global_watcher = create_global_setting_for(create(:user), :watch)
+ group.add_users([@g_watcher, @g_global_watcher], :master)
+ group
+ end
+
def create_global_setting_for(user, level)
setting = user.global_notification_setting
setting.level = level
@@ -1226,9 +1246,9 @@ describe NotificationService, services: true do
user
end
- def create_user_with_notification(level, username)
+ def create_user_with_notification(level, username, resource = project)
user = create(:user, username: username)
- setting = user.notification_settings_for(project)
+ setting = user.notification_settings_for(resource)
setting.level = level
setting.save
diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb
new file mode 100644
index 00000000000..aa63fe3a5c1
--- /dev/null
+++ b/spec/services/pages_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe PagesService, services: true do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:service) { PagesService.new(data) }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ context 'execute asynchronously for pages job' do
+ before { build.name = 'pages' }
+
+ context 'on success' do
+ before { build.success }
+
+ it 'executes worker' do
+ expect(PagesWorker).to receive(:perform_async)
+ service.execute
+ end
+ end
+
+ %w(pending running failed canceled).each do |status|
+ context "on #{status}" do
+ before { build.status = status }
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+ end
+ end
+
+ context 'for other jobs' do
+ before do
+ build.name = 'other job'
+ build.success
+ end
+
+ it 'does not execute worker' do
+ expect(PagesWorker).not_to receive(:perform_async)
+ service.execute
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index a1539b69401..af515ad2e0e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -90,10 +90,6 @@ describe Projects::CreateService, '#execute', services: true do
end
context 'global builds_enabled true does enable CI by default' do
- before do
- project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
- end
-
it { is_expected.to be_truthy }
end
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 90771825f5c..3faa88c00a1 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -9,12 +9,27 @@ describe Projects::DestroyService, services: true do
shared_examples 'deleting the project' do
it 'deletes the project' do
- expect(Project.all).not_to include(project)
+ expect(Project.unscoped.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
expect(Dir.exist?(remove_path)).to be_falsey
end
end
+ shared_examples 'deleting the project with pipeline and build' do
+ context 'with pipeline and build' do # which has optimistic locking
+ let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ perform_enqueued_jobs do
+ destroy_project(project, user, {})
+ end
+ end
+
+ it_behaves_like 'deleting the project'
+ end
+ end
+
context 'Sidekiq inline' do
before do
# Run sidekiq immediatly to check that renamed repository will be removed
@@ -35,30 +50,24 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
- context 'async delete of project with private issue visibility' do
- let!(:async) { true }
+ context 'with async_execute' do
+ let(:async) { true }
- before do
- project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
- # Run sidekiq immediately to check that renamed repository will be removed
- Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ context 'async delete of project with private issue visibility' do
+ before do
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ # Run sidekiq immediately to check that renamed repository will be removed
+ Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ end
+
+ it_behaves_like 'deleting the project'
end
- it_behaves_like 'deleting the project'
+ it_behaves_like 'deleting the project with pipeline and build'
end
- context 'delete with pipeline' do # which has optimistic locking
- let!(:pipeline) { create(:ci_pipeline, project: project) }
-
- before do
- expect(project).to receive(:destroy!).and_call_original
-
- perform_enqueued_jobs do
- destroy_project(project, user, {})
- end
- end
-
- it_behaves_like 'deleting the project'
+ context 'with execute' do
+ it_behaves_like 'deleting the project with pipeline and build'
end
context 'container registry' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 1540b90163a..5c6fbea8d0e 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do
before do
allow_any_instance_of(Gitlab::UploadsTransfer).
to receive(:move_project).and_return(true)
+ allow_any_instance_of(Gitlab::PagesTransfer).
+ to receive(:move_project).and_return(true)
group.add_owner(user)
@result = transfer_project(project, user, group)
end
@@ -81,4 +83,30 @@ describe Projects::TransferService, services: true do
transfer_project(project, user, group)
end
end
+
+ describe 'refreshing project authorizations' do
+ let(:group) { create(:group) }
+ let(:owner) { project.namespace.owner }
+ let(:group_member) { create(:user) }
+
+ before do
+ group.add_user(owner, GroupMember::MASTER)
+ group.add_user(group_member, GroupMember::DEVELOPER)
+ end
+
+ it 'refreshes the permissions of the old and new namespace' do
+ transfer_project(project, owner, group)
+
+ expect(group_member.authorized_projects).to include(project)
+ expect(owner.authorized_projects).to include(project)
+ end
+
+ it 'only schedules a single job for every user' do
+ expect(UserProjectAccessChangedService).to receive(:new).
+ with([owner.id, group_member.id]).
+ and_call_original
+
+ transfer_project(project, owner, group)
+ end
+ end
end
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
new file mode 100644
index 00000000000..8b329bc21c3
--- /dev/null
+++ b/spec/services/projects/update_pages_configuration_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Projects::UpdatePagesConfigurationService, services: true do
+ let(:project) { create(:empty_project) }
+ subject { described_class.new(project) }
+
+ describe "#update" do
+ let(:file) { Tempfile.new('pages-test') }
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ it 'updates the .update file' do
+ # Access this reference to ensure scoping works
+ Projects::Settings # rubocop:disable Lint/Void
+ expect(subject).to receive(:pages_config_file).and_return(file.path)
+ expect(subject).to receive(:reload_daemon).and_call_original
+
+ expect(subject.execute).to eq({ status: :success })
+ end
+ end
+end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
new file mode 100644
index 00000000000..411b22a0fb8
--- /dev/null
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -0,0 +1,80 @@
+require "spec_helper"
+
+describe Projects::UpdatePagesService do
+ let(:project) { create :project }
+ let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha }
+ let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' }
+ let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+
+ subject { described_class.new(project, build) }
+
+ before do
+ project.remove_pages
+ end
+
+ %w(tar.gz zip).each do |format|
+ context "for valid #{format}" do
+ let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") }
+ let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") }
+ let(:metadata) do
+ filename = Rails.root + "spec/fixtures/pages.#{format}.meta"
+ fixture_file_upload(filename) if File.exist?(filename)
+ end
+
+ before do
+ build.update_attributes(artifacts_file: file)
+ build.update_attributes(artifacts_metadata: metadata)
+ end
+
+ it 'succeeds' do
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ end
+
+ it 'limits pages size' do
+ stub_application_setting(max_pages_size: 1)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'removes pages after destroy' do
+ expect(PagesWorker).to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ project.destroy
+ expect(project.pages_deployed?).to be_falsey
+ end
+
+ it 'fails if sha on branch is not latest' do
+ pipeline.update_attributes(sha: 'old_sha')
+ build.update_attributes(artifacts_file: file)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for empty file fails' do
+ build.update_attributes(artifacts_file: empty_file)
+ expect(execute).not_to eq(:success)
+ end
+ end
+ end
+
+ it 'fails to remove project pages when no pages is deployed' do
+ expect(PagesWorker).not_to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ project.destroy
+ end
+
+ it 'fails if no artifacts' do
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for invalid archive' do
+ build.update_attributes(artifacts_file: invalid_file)
+ expect(execute).not_to eq(:success)
+ end
+
+ def execute
+ subject.execute[:status]
+ end
+end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index bd89c4a7c11..bed1031e40a 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -41,6 +41,25 @@ describe 'Search::GlobalService', services: true do
results = context.execute
expect(results.objects('projects')).to match_array [found_project]
end
+
+ context 'nested group' do
+ let!(:nested_group) { create(:group, :nested) }
+ let!(:project) { create(:project, namespace: nested_group) }
+
+ before { project.add_master(user) }
+
+ it 'returns result from nested group' do
+ context = Search::GlobalService.new(user, search: project.path)
+ results = context.execute
+ expect(results.objects('projects')).to match_array [project]
+ end
+
+ it 'returns result from descendants when search inside group' do
+ context = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent)
+ results = context.execute
+ expect(results.objects('projects')).to match_array [project]
+ end
+ end
end
end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 66fc8fc360b..0b0925983eb 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
end
+
+ context '/target_branch command' do
+ let(:non_empty_project) { create(:project) }
+ let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
+ let(:service) { described_class.new(non_empty_project, developer)}
+
+ it 'updates target_branch if /target_branch command is executed' do
+ _, updates = service.execute('/target_branch merge-test', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ it 'handles blanks around param' do
+ _, updates = service.execute('/target_branch merge-test ', merge_request)
+
+ expect(updates).to eq(target_branch: 'merge-test')
+ end
+
+ context 'ignores command with no argument' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+
+ context 'ignores non-existing target branch' do
+ it_behaves_like 'empty command' do
+ let(:content) { '/target_branch totally_non_existing_branch' }
+ let(:issuable) { another_merge_request }
+ end
+ end
+ end
end
end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
new file mode 100644
index 00000000000..271c17dd8c0
--- /dev/null
+++ b/spec/services/spam_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe SpamService, services: true do
+ describe '#check' do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:request) { double(:request, env: {}) }
+
+ def check_spam(issue, request)
+ described_class.new(issue, request).check
+ end
+
+ context 'when indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: true)) }
+
+ it 'returns false when request is missing' do
+ expect(check_spam(issue, nil)).to be_falsey
+ end
+
+ it 'returns false when issue is not public' do
+ issue = create(:issue, project: create(:project, :private))
+
+ expect(check_spam(issue, request)).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(check_spam(issue, request)).to be_truthy
+ end
+
+ it 'creates a spam log' do
+ expect { check_spam(issue, request) }.to change { SpamLog.count }.from(0).to(1)
+ end
+ end
+
+ context 'when not indicated as spam by akismet' do
+ before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) }
+
+ it 'returns false' do
+ expect(check_spam(issue, request)).to be_falsey
+ end
+
+ it 'does not create a spam log' do
+ expect { check_spam(issue, request) }.not_to change { SpamLog.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 9f5a0ac4ec6..7f027ae02a2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -245,6 +245,8 @@ describe SystemNoteService, services: true do
end
describe '.change_title' do
+ let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+
subject { described_class.change_title(noteable, project, author, 'Old title') }
context 'when noteable responds to `title`' do
@@ -252,7 +254,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
expect(subject.note).
- to eq "changed title from **{-Old title-}** to **{+#{noteable.title}+}**"
+ to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
end
end
end
@@ -416,6 +418,45 @@ describe SystemNoteService, services: true do
to be_truthy
end
end
+
+ context 'when noteable is an Issue' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'is truthy when issue is closed' do
+ issue.close
+
+ expect(described_class.cross_reference_disallowed?(issue, project.commit)).
+ to be_truthy
+ end
+
+ it 'is falsey when issue is open' do
+ expect(described_class.cross_reference_disallowed?(issue, project.commit)).
+ to be_falsy
+ end
+ end
+
+ context 'when noteable is a Merge Request' do
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ it 'is truthy when merge request is closed' do
+ allow(merge_request).to receive(:closed?).and_return(:true)
+
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_truthy
+ end
+
+ it 'is truthy when merge request is merged' do
+ allow(merge_request).to receive(:closed?).and_return(:true)
+
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_truthy
+ end
+
+ it 'is falsey when merge request is open' do
+ expect(described_class.cross_reference_disallowed?(merge_request, project.commit)).
+ to be_falsy
+ end
+ end
end
describe '.cross_reference_exists?' do
@@ -750,13 +791,13 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
- expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h"
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
end
context 'without a time estimate' do
it 'sets the note text' do
- expect(subject.note).to eq "Removed time estimate on this issue"
+ expect(subject.note).to eq "removed time estimate"
end
end
end
@@ -780,7 +821,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(277200)
- expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request"
+ expect(subject.note).to eq "added 1w 4d 5h of time spent"
end
end
@@ -788,7 +829,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(-277200)
- expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request"
+ expect(subject.note).to eq "subtracted 1w 4d 5h of time spent"
end
end
@@ -796,7 +837,7 @@ describe SystemNoteService, services: true do
it 'sets the note text' do
spend_time!(:reset)
- expect(subject.note).to eq "Removed time spent on this merge request"
+ expect(subject.note).to eq "removed time spent"
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 13d584a8975..4320365ab57 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -9,7 +9,9 @@ describe TodoService, services: true do
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
let(:project) { create(:project) }
- let(:mentions) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
+ let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
@@ -21,8 +23,10 @@ describe TodoService, services: true do
describe 'Issues' do
let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
let(:unassigned_issue) { create(:issue, project: project, assignee: nil) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) }
+ let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) }
describe '#new_issue' do
it 'creates a todo if assigned' do
@@ -52,6 +56,26 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_issue(addressed_issue, author)
+
+ should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
+ it 'creates correct todos for each valid user based on the type of mention' do
+ issue.update(description: directly_addressed_and_mentioned)
+
+ service.new_issue(issue, author)
+
+ should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
+ end
+
it 'does not create todo if user can not see the issue when issue is confidential' do
service.new_issue(confidential_issue, john_doe)
@@ -63,6 +87,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo if user cannot see the issue when issue is confidential' do
+ service.new_issue(addressed_confident_issue, john_doe)
+
+ should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::ASSIGNED)
+ should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
context 'when a private group is mentioned' do
let(:group) { create :group, :private }
let(:project) { create :project, :private, group: group }
@@ -94,12 +129,38 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ issue.update(description: directly_addressed_and_mentioned)
+
+ service.update_issue(issue, author)
+
+ should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.update_issue(addressed_issue, author)
+
+ should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
end
+ it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author)
+
+ expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count)
+ end
+
it 'does not create todo if user can not see the issue when issue is confidential' do
service.update_issue(confidential_issue, john_doe)
@@ -111,6 +172,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
+ it 'does not create a directly addressed todo if user can not see the issue when issue is confidential' do
+ service.update_issue(addressed_confident_issue, john_doe)
+
+ should_create_todo(user: author, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: assignee, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: member, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_confident_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
context 'issues with a task list' do
it 'does not create todo when tasks are marked as completed' do
issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
@@ -125,6 +197,19 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo when tasks are marked as completed' do
+ addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
+
+ service.update_issue(addressed_issue, author)
+
+ should_not_create_todo(user: admin, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: assignee, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not raise an error when description not change' do
issue.update(title: 'Sample')
@@ -244,8 +329,11 @@ describe TodoService, services: true do
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) }
+ let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) }
let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) }
+ let(:addressed_note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: directly_addressed) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: mentions) }
+ let(:addressed_note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project, note: directly_addressed) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) }
let(:system_note) { create(:system_note, project: project, noteable: issue) }
@@ -276,6 +364,26 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ note.update(note: directly_addressed_and_mentioned)
+
+ service.new_note(note, john_doe)
+
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: note)
+ should_create_todo(user: admin, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_note(addressed_note, john_doe)
+
+ should_create_todo(user: member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: author, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note)
+ end
+
it 'does not create todo if user can not see the issue when leaving a note on a confidential issue' do
service.new_note(note_on_confidential_issue, john_doe)
@@ -287,6 +395,17 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
+ it 'does not create a directly addressed todo if user can not see the issue when leaving a note on a confidential issue' do
+ service.new_note(addressed_note_on_confidential_issue, john_doe)
+
+ should_create_todo(user: author, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: assignee, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
+ end
+
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
service.new_note(note_on_commit, john_doe)
@@ -296,6 +415,15 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
+ it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(addressed_note_on_commit, john_doe)
+
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ end
+
it 'does not create todo when leaving a note on snippet' do
should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) }
end
@@ -324,6 +452,7 @@ describe TodoService, services: true do
describe 'Merge Requests' do
let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
describe '#new_merge_request' do
@@ -350,6 +479,25 @@ describe TodoService, services: true do
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'creates a todo for each valid user based on the type of mention' do
+ mr_assigned.update(description: directly_addressed_and_mentioned)
+
+ service.new_merge_request(mr_assigned, author)
+
+ should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.new_merge_request(addressed_mr_assigned, author)
+
+ should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#update_merge_request' do
@@ -363,12 +511,37 @@ describe TodoService, services: true do
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
+ it 'creates a todo for each valid user based on the type of mention' do
+ mr_assigned.update(description: directly_addressed_and_mentioned)
+
+ service.update_merge_request(mr_assigned, author)
+
+ should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user' do
+ service.update_merge_request(addressed_mr_assigned, author)
+
+ should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not create a todo if user was already mentioned' do
create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
+ it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author)
+
+ expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
+ end
+
context 'with a task list' do
it 'does not create todo when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
@@ -384,6 +557,20 @@ describe TodoService, services: true do
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+ it 'does not create directly addressed todo when tasks are marked as completed' do
+ addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
+
+ service.update_merge_request(addressed_mr_assigned, author)
+
+ should_not_create_todo(user: admin, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: assignee, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
it 'does not raise an error when description not change' do
mr_assigned.update(title: 'Sample')
@@ -436,6 +623,11 @@ describe TodoService, services: true do
service.reassigned_merge_request(mr_assigned, author)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'does not create a directly addressed todo for guests' do
+ service.reassigned_merge_request(addressed_mr_assigned, author)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#merge_merge_request' do
@@ -452,6 +644,11 @@ describe TodoService, services: true do
service.merge_merge_request(mr_assigned, john_doe)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
end
+
+ it 'does not create directly addressed todo for guests' do
+ service.merge_merge_request(addressed_mr_assigned, john_doe)
+ should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
end
describe '#new_award_emoji' do
@@ -509,6 +706,7 @@ describe TodoService, services: true do
describe '#new_note' do
let(:mention) { john_doe.to_reference }
let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+ let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") }
let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
it 'creates a todo for mentioned user on new diff note' do
@@ -517,6 +715,12 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request)
end
+ it 'creates a directly addressed todo for addressed user on new diff note' do
+ service.new_note(addressed_diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::DIRECTLY_ADDRESSED, note: addressed_diff_note_on_merge_request)
+ end
+
it 'creates a todo for mentioned user on legacy diff note' do
service.new_note(legacy_diff_note_on_merge_request, author)
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/users/destroy_spec.rb
index 418a12a83a9..46e58393218 100644
--- a/spec/services/delete_user_service_spec.rb
+++ b/spec/services/users/destroy_spec.rb
@@ -1,15 +1,16 @@
require 'spec_helper'
-describe DeleteUserService, services: true do
+describe Users::DestroyService, services: true do
describe "Deletes a user and all their personal projects" do
let!(:user) { create(:user) }
let!(:current_user) { create(:user) }
let!(:namespace) { create(:namespace, owner: user) }
let!(:project) { create(:project, namespace: namespace) }
+ let(:service) { described_class.new(current_user) }
context 'no options are given' do
it 'deletes the user' do
- user_data = DeleteUserService.new(current_user).execute(user)
+ user_data = service.execute(user)
expect { user_data['email'].to eq(user.email) }
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
@@ -19,7 +20,7 @@ describe DeleteUserService, services: true do
it 'will delete the project in the near future' do
expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
- DeleteUserService.new(current_user).execute(user)
+ service.execute(user)
end
end
@@ -30,7 +31,7 @@ describe DeleteUserService, services: true do
before do
solo_owned.group_members = [member]
- DeleteUserService.new(current_user).execute(user)
+ service.execute(user)
end
it 'does not delete the user' do
@@ -45,7 +46,7 @@ describe DeleteUserService, services: true do
before do
solo_owned.group_members = [member]
- DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true)
+ service.execute(user, delete_solo_owned_groups: true)
end
it 'deletes solo owned groups' do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index ab38dac65c5..5fda7c63cdb 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -35,6 +35,7 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitForAjax, type: :feature
config.include StubConfiguration
config.include EmailHelpers, type: :mailer
config.include TestEnv
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 68b196d9033..ae6e708cf87 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -17,8 +17,8 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
- def api(path, user = nil)
- "/api/#{API::API.version}#{path}" +
+ def api(path, user = nil, version: API::API.version)
+ "/api/#{version}#{path}" +
# Normalize query string
(path.index('?') ? '' : '?') +
@@ -31,6 +31,11 @@ module ApiHelpers
end
end
+ # Temporary helper method for simplifying V3 exclusive API specs
+ def v3_api(path, user = nil)
+ api(path, user, version: 'v3')
+ end
+
def ci_api(path, user = nil)
"/ci/api/v1/#{path}" +
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 75c95d70951..6ed55289ed9 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -35,7 +35,13 @@ module CycleAnalyticsHelpers
project.repository.add_branch(user, source_branch, 'master')
end
- sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ sha = project.repository.commit_file(
+ user,
+ random_git_name,
+ 'content',
+ message: 'commit message',
+ branch_name: source_branch,
+ update: false)
project.repository.commit(sha)
opts = {
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 10b90b40ba7..19b32c84d81 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -63,22 +63,20 @@ module CycleAnalyticsHelpers
# test case.
allow(self).to receive(:project) { other_project }
- 5.times do
- data = data_fn[self]
- start_time = Time.now
- end_time = rand(1..10).days.from_now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
- end_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(end_time) { condition_fn[self, data] }
- end
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
end
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
# Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original
@@ -114,17 +112,15 @@ module CycleAnalyticsHelpers
context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- end_time = rand(1..10).days.from_now
-
- end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
- Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
- Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
end
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
expect(subject[phase].median).to be_nil
end
end
@@ -133,17 +129,15 @@ module CycleAnalyticsHelpers
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
- 5.times do
- data = data_fn[self]
- start_time = Time.now
-
- start_time_conditions.each do |condition_name, condition_fn|
- Timecop.freeze(start_time) { condition_fn[self, data] }
- end
+ data = data_fn[self]
+ start_time = Time.now
- post_fn[self, data] if post_fn
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
end
+ post_fn[self, data] if post_fn
+
expect(subject[phase].median).to be_nil
end
end
diff --git a/spec/support/drag_to_helper.rb b/spec/support/drag_to_helper.rb
new file mode 100644
index 00000000000..0c0659d3ecd
--- /dev/null
+++ b/spec/support/drag_to_helper.rb
@@ -0,0 +1,13 @@
+module DragTo
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body')
+ evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until drag_active?
+ end
+ end
+
+ def drag_active?
+ page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 1b0a4583f5c..944ea30656f 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -35,7 +35,7 @@ module ExportFileHelper
project: project,
commit_id: ci_pipeline.sha)
- create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:event, :created, target: milestone, project: project, author: user)
create(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
new file mode 100644
index 00000000000..dac94dfc31e
--- /dev/null
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -0,0 +1,35 @@
+shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
+ before do
+ @issuable_ids = []
+
+ 2.times do
+ if issuable_type == :issue
+ issuable = create(issuable_type, project: project)
+ else
+ issuable = create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ end
+
+ @issuable_ids << issuable.id
+
+ issuable.id.times { create(:note, noteable: issuable, project: issuable.project) }
+ (issuable.id + 1).times { create(:award_emoji, :downvote, awardable: issuable) }
+ (issuable.id + 2).times { create(:award_emoji, :upvote, awardable: issuable) }
+ end
+ end
+
+ it "creates indexed meta-data object for issuable notes and votes count" do
+ if action
+ get action
+ else
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+ end
+
+ meta_data = assigns(:issuable_meta_data)
+
+ @issuable_ids.each do |id|
+ expect(meta_data[id].notes_count).to eq(id)
+ expect(meta_data[id].downvotes).to eq(id + 1)
+ expect(meta_data[id].upvotes).to eq(id + 2)
+ end
+ end
+end
diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb
index 6c4c246a68b..444612cf871 100644
--- a/spec/support/kubernetes_helpers.rb
+++ b/spec/support/kubernetes_helpers.rb
@@ -43,7 +43,8 @@ module KubernetesHelpers
url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
- created_at: DateTime.parse(pod['metadata']['creationTimestamp'])
+ created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
+ max_session_time: 0
}
terminal[:ca_pem] = service.ca_pem if service.ca_pem.present?
terminal
diff --git a/spec/support/matchers/match_file.rb b/spec/support/matchers/match_file.rb
new file mode 100644
index 00000000000..d1888b3376a
--- /dev/null
+++ b/spec/support/matchers/match_file.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :match_file do |expected|
+ match do |actual|
+ expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected)))
+ end
+end
diff --git a/spec/support/matchers/satisfy_matchers.rb b/spec/support/matchers/satisfy_matchers.rb
new file mode 100644
index 00000000000..585915bac93
--- /dev/null
+++ b/spec/support/matchers/satisfy_matchers.rb
@@ -0,0 +1,19 @@
+# These matchers are a syntactic hack to provide more readable expectations for
+# an Enumerable object.
+#
+# They take advantage of the `all?`, `none?`, and `one?` methods, and the fact
+# that RSpec provides a `be_something` matcher for all predicates.
+#
+# Example:
+#
+# # Ensure exactly one object in an Array satisfies a condition
+# expect(users.one? { |u| u.admin? }).to eq true
+#
+# # The same thing, but using the `be_one` matcher
+# expect(users).to be_one { |u| u.admin? }
+#
+# # The same thing again, but using `satisfy_one` for improved readability
+# expect(users).to satisfy_one { |u| u.admin? }
+RSpec::Matchers.alias_matcher :satisfy_all, :be_all
+RSpec::Matchers.alias_matcher :satisfy_none, :be_none
+RSpec::Matchers.alias_matcher :satisfy_one, :be_one
diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb
index 93c0267d2db..4f0c745b7ee 100644
--- a/spec/support/services/issuable_create_service_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_shared_examples.rb
@@ -31,8 +31,8 @@ shared_examples 'issuable create service' do
context "when issuable feature is private" do
before do
- project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
- project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
+ merge_requests_access_level: ProjectFeature::PRIVATE)
end
levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index dd54b0addda..c64574679b6 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -58,7 +58,7 @@ shared_examples 'new issuable record that supports slash commands' do
let(:example_params) do
{
assignee: create(:user),
- milestone_id: double(:milestone),
+ milestone_id: 1,
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
}
end
diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb
index df483afa0e3..0d91fe5fd5d 100644
--- a/spec/support/slash_commands_helpers.rb
+++ b/spec/support/slash_commands_helpers.rb
@@ -3,7 +3,7 @@ module SlashCommandsHelpers
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
- click_button 'Comment'
+ find('.comment-btn').trigger('click')
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 90f1a9c8798..b87232a350b 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -36,7 +36,8 @@ module TestEnv
'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
'deleted-image-test' => '6c17798',
- 'wip' => 'b9238ee'
+ 'wip' => 'b9238ee',
+ 'csv' => '3dd0896'
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb
index 02657684b57..52f4fabdc47 100644
--- a/spec/support/time_tracking_shared_examples.rb
+++ b/spec/support/time_tracking_shared_examples.rb
@@ -77,6 +77,6 @@ end
def submit_time(slash_command)
fill_in 'note[note]', with: slash_command
- click_button 'Comment'
+ find('.comment-btn').trigger('click')
wait_for_ajax
end
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
new file mode 100644
index 00000000000..c32f9a740b7
--- /dev/null
+++ b/spec/tasks/config_lint_spec.rb
@@ -0,0 +1,27 @@
+require 'rake_helper'
+Rake.application.rake_require 'tasks/config_lint'
+
+describe ConfigLint do
+ let(:files){ ['lib/support/fake.sh'] }
+
+ it 'errors out if any bash scripts have errors' do
+ expect { ConfigLint.run(files){ system('exit 1') } }.to raise_error(SystemExit)
+ end
+
+ it 'passes if all scripts are fine' do
+ expect { ConfigLint.run(files){ system('exit 0') } }.not_to raise_error
+ end
+end
+
+describe 'config_lint rake task' do
+ before(:each) do
+ # Prevent `system` from actually being called
+ allow(Kernel).to receive(:system).and_return(true)
+ end
+
+ it 'runs lint on shell scripts' do
+ expect(Kernel).to receive(:system).with('bash', '-n', 'lib/support/init.d/gitlab')
+
+ run_rake_task('config_lint')
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bc751d20ce1..df8a47893f9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds artifacts lfs registry}.each do |subtask|
+ %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke)
expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
@@ -202,7 +203,7 @@ describe 'gitlab:app namespace rake task' do
it 'sets correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
@@ -210,14 +211,15 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('repositories/')
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
it 'deletes temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}')
)
expect(temp_dirs).to be_empty
@@ -304,7 +306,7 @@ describe 'gitlab:app namespace rake task' do
it "does not contain skipped item" do
tar_contents, _exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz}
)
expect(tar_contents).to match('db/')
@@ -312,6 +314,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('builds.tar.gz')
expect(tar_contents).to match('artifacts.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
+ expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
expect(tar_contents).not_to match('repositories/')
end
@@ -327,6 +330,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+ expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke
expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
index 80fc8c48fed..8d1cff7a261 100644
--- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
+++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb
@@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do
Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting"
end
- it 'should run the task without errors' do
+ it 'runs the task without errors' do
expect { run_rake_task }.not_to raise_error
end
end
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
deleted file mode 100644
index 643b161cdf4..00000000000
--- a/spec/teaspoon_env.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-Teaspoon.configure do |config|
- # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to
- # `http://localhost:3000/jasmine` to run your tests.
- config.mount_at = "/teaspoon"
-
- # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can
- # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`).
- # Note: Defaults to `Rails.root` if nil.
- config.root = nil
-
- # Paths that will be appended to the Rails assets paths
- # Note: Relative to `config.root`.
- config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"]
-
- # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will
- # be rendered as fixtures.
- config.fixture_paths = ["spec/javascripts/fixtures"]
-
- # SUITES
- #
- # You can modify the default suite configuration and create new suites here. Suites are isolated from one another.
- #
- # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can
- # omit various directives and the ones defined in the default suite will be used.
- #
- # To run a specific suite
- # - in the browser: http://localhost/teaspoon/[suite_name]
- # - with the rake task: rake teaspoon suite=[suite_name]
- # - with the cli: teaspoon --suite=[suite_name]
- config.suite do |suite|
- # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for
- # you -- which you can override with the directives below. This should be specified first, as it can override other
- # directives.
- # Note: If no version is specified, the latest is assumed.
- #
- # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0
- suite.use_framework :jasmine, "2.2.0"
-
- # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
- # files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
- suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
-
- # Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
- # suite.javascripts = []
-
- # You can include your own stylesheets if you want to change how Teaspoon looks.
- # Note: Spec related CSS can and should be loaded using fixtures.
- # suite.stylesheets = ["teaspoon"]
-
- # This suites spec helper, which can require additional support files. This file is loaded before any of your test
- # files are loaded.
- suite.helper = "spec_helper"
-
- # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating
- # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance.
- #
- # Available: boot, boot_require_js
- suite.boot_partial = "boot"
-
- # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure.
- suite.body_partial = "body"
-
- # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a
- # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name.
- # suite.hook :fixtures, &proc{}
-
- # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated
- # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default,
- # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files.
- # suite.expand_assets = true
- end
-
- # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also
- # be run in the default suite -- but can be focused into a more specific suite.
- # config.suite :targeted do |suite|
- # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}"
- # end
-
- # CONSOLE RUNNER SPECIFIC
- #
- # These configuration directives are applicable only when running via the rake task or command line interface. These
- # directives can be overridden using the command line interface arguments or with ENV variables when using the rake
- # task.
- #
- # Command Line Interface:
- # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js
- #
- # Rake:
- # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite
-
- # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver.
- #
- # Available: :phantomjs, :selenium, :capybara_webkit
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver = :phantomjs
-
- # Specify additional options for the driver.
- #
- # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS
- # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver
- # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit
- # config.driver_options = nil
-
- # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be
- # considered a failure. This is to avoid issues that can arise where tests stall.
- # config.driver_timeout = 180
-
- # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used.
- # config.server = nil
-
- # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port.
- # config.server_port = nil
-
- # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may
- # want to lower this if you know it shouldn't take long to start.
- # config.server_timeout = 20
-
- # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have
- # several suites, but in environments like CI this may not be desirable.
- # config.fail_fast = true
-
- # Specify the formatters to use when outputting the results.
- # Note: Output files can be specified by using `"junit>/path/to/output.xml"`.
- #
- # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity
- # config.formatters = [:dot]
-
- # Specify if you want color output from the formatters.
- # config.color = true
-
- # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to
- # remove them, but in verbose applications this may not be desirable.
- # config.suppress_log = false
-
- # COVERAGE REPORTS / THRESHOLD ASSERTIONS
- #
- # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and
- # display coverage statistics.
- #
- # Coverage configurations are similar to suites. You can define several, and use different ones under different
- # conditions.
- #
- # To run with a specific coverage configuration
- # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name]
- # - with the cli: teaspoon --coverage=[coverage_name]
-
- # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
- # on the CLI.
- # Set this to "true" or the name of your coverage config.
- config.use_coverage = true
-
- # You can have multiple coverage configs by passing a name to config.coverage.
- # e.g. config.coverage :ci do |coverage|
- # The default coverage config name is :default.
- config.coverage do |coverage|
- # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
- #
- # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- coverage.reports = ["text-summary", "html"]
-
- # The path that the coverage should be written to - when there's an artifact to write to disk.
- # Note: Relative to `config.root`.
- coverage.output_path = "coverage-javascript"
-
- # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
- # default excludes assets from vendor, gems and support libraries.
- coverage.ignore = [%r{vendor/}, %r{spec/javascripts/(?!helpers/)}]
-
- # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
- # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
- # coverage.statements = nil
- # coverage.functions = nil
- # coverage.branches = nil
- # coverage.lines = nil
- 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 44870cfcfb3..b6f6e7b7a2b 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -15,7 +15,7 @@ describe 'projects/builds/show', :view do
allow(view).to receive(:can?).and_return(true)
end
- describe 'build information in header' do
+ describe 'job information in header' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
end
@@ -28,11 +28,11 @@ describe 'projects/builds/show', :view do
expect(rendered).to have_css('.ci-status.ci-success', text: 'passed')
end
- it 'does not render a link to the build' do
+ it 'does not render a link to the job' do
expect(rendered).not_to have_link('passed')
end
- it 'shows build id' do
+ it 'shows job id' do
expect(rendered).to have_css('.js-build-id', text: build.id)
end
@@ -45,8 +45,8 @@ describe 'projects/builds/show', :view do
end
end
- describe 'environment info in build view' do
- context 'build with latest deployment' do
+ describe 'environment info in job view' do
+ context 'job with latest deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging')
end
@@ -57,7 +57,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is the most recent deployment'
+ expected_text = 'This job is the most recent deployment'
render
expect(rendered).to have_css(
@@ -65,7 +65,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build with outdated deployment' do
+ context 'job with outdated deployment' do
let(:build) do
create(:ci_build, :success, environment: 'staging', pipeline: pipeline)
end
@@ -87,7 +87,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is an out-of-date deployment ' \
+ expected_text = 'This job is an out-of-date deployment ' \
"to staging.\nView the most recent deployment ##{second_deployment.iid}."
render
@@ -95,7 +95,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build failed to deploy' do
+ context 'job failed to deploy' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -105,7 +105,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed.'
+ expected_text = 'The deployment of this job to staging did not succeed.'
render
expect(rendered).to have_css(
@@ -113,7 +113,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build will deploy' do
+ context 'job will deploy' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -124,7 +124,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -137,7 +137,7 @@ describe 'projects/builds/show', :view do
end
it 'shows that deployment will be overwritten' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -150,7 +150,7 @@ describe 'projects/builds/show', :view do
context 'when environment does not exist' do
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -161,7 +161,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that failed to deploy and environment has not been created' do
+ context 'job that failed to deploy and environment has not been created' do
let(:build) do
create(:ci_build, :failed, environment: 'staging', pipeline: pipeline)
end
@@ -171,7 +171,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'The deployment of this build to staging did not succeed'
+ expected_text = 'The deployment of this job to staging did not succeed'
render
expect(rendered).to have_css(
@@ -179,7 +179,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'build that will deploy and environment has not been created' do
+ context 'job that will deploy and environment has not been created' do
let(:build) do
create(:ci_build, :running, environment: 'staging', pipeline: pipeline)
end
@@ -189,7 +189,7 @@ describe 'projects/builds/show', :view do
end
it 'shows deployment message' do
- expected_text = 'This build is creating a deployment to staging'
+ expected_text = 'This job is creating a deployment to staging'
render
expect(rendered).to have_css(
@@ -200,7 +200,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'when build is running' do
+ context 'when job is running' do
before do
build.run!
render
@@ -211,7 +211,7 @@ describe 'projects/builds/show', :view do
end
end
- context 'when build is not running' do
+ context 'when job is not running' do
before do
build.success!
render
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 14c56521280..0765573408c 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -5,14 +5,14 @@ describe DeleteUserWorker do
let!(:current_user) { create(:user) }
it "calls the DeleteUserWorker with the params it was given" do
- expect_any_instance_of(DeleteUserService).to receive(:execute).
+ expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, {})
DeleteUserWorker.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
- expect_any_instance_of(DeleteUserService).to receive(:execute).
+ expect_any_instance_of(Users::DestroyService).to receive(:execute).
with(user, test: "test")
DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test")
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index e471a68a49a..5ef8cf1105b 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -107,7 +107,8 @@ describe GitGarbageCollectWorker do
tree: old_commit.tree,
parents: [old_commit],
)
- project.repository.update_ref!(
+ GitOperationService.new(nil, project.repository).send(
+ :update_ref,
"refs/heads/#{SecureRandom.hex(6)}",
new_commit_sha,
Gitlab::Git::BLANK_SHA
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 984acdade36..5919b99a6ed 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -74,7 +74,7 @@ describe PostReceive do
context "webhook" do
it "fetches the correct project" do
- expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
+ expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
@@ -89,7 +89,7 @@ describe PostReceive do
end
it "asks the project to trigger all hooks" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
@@ -97,7 +97,7 @@ describe PostReceive do
end
it "enqueues a UpdateMergeRequestsWorker job" do
- allow(Project).to receive(:find_with_namespace).and_return(project)
+ allow(Project).to receive(:find_by_full_path).and_return(project)
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 1b910d9b91e..1f4c39eb64a 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -8,14 +8,14 @@ describe ProjectDestroyWorker do
describe "#perform" do
it "deletes the project" do
- subject.perform(project.id, project.owner, {})
+ subject.perform(project.id, project.owner.id, {})
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
end
it "deletes the project but skips repo deletion" do
- subject.perform(project.id, project.owner, { "skip_repo" => true })
+ subject.perform(project.id, project.owner.id, { "skip_repo" => true })
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_truthy
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 59cfb2c8e3a..d2609d21546 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
subject { described_class.new }
it 'passes when the project has no push events' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
project.events.destroy_all
break_repo(project)
@@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'fails if the wiki repository is broken' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
project.create_wiki
# Test sanity: everything should be fine before the wiki repo is broken
@@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'skips wikis when disabled' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED)
+ project = create(:project_empty_repo, :wiki_disabled)
# Make sure the test would fail if the wiki repo was checked
break_wiki(project)
@@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
end
it 'creates missing wikis' do
- project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED)
+ project = create(:project_empty_repo, :wiki_enabled)
FileUtils.rm_rf(wiki_path(project))
subject.perform(project.id)
diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js
index f5dc4abcd80..2c9b4825443 100644
--- a/vendor/assets/javascripts/date.format.js
+++ b/vendor/assets/javascripts/date.format.js
@@ -11,115 +11,122 @@
* The date defaults to the current date/time.
* The mask defaults to dateFormat.masks.default.
*/
+ (function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.dateFormat = factory());
+ }(this, (function () { 'use strict';
+ var dateFormat = function () {
+ var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
+ timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
+ timezoneClip = /[^-+\dA-Z]/g,
+ pad = function (val, len) {
+ val = String(val);
+ len = len || 2;
+ while (val.length < len) val = "0" + val;
+ return val;
+ };
-var dateFormat = function () {
- var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
- timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
- timezoneClip = /[^-+\dA-Z]/g,
- pad = function (val, len) {
- val = String(val);
- len = len || 2;
- while (val.length < len) val = "0" + val;
- return val;
- };
+ // Regexes and supporting functions are cached through closure
+ return function (date, mask, utc) {
+ var dF = dateFormat;
- // Regexes and supporting functions are cached through closure
- return function (date, mask, utc) {
- var dF = dateFormat;
+ // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
+ if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
+ mask = date;
+ date = undefined;
+ }
- // You can't provide utc if you skip other args (use the "UTC:" mask prefix)
- if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
- mask = date;
- date = undefined;
- }
+ // Passing date through Date applies Date.parse, if necessary
+ date = date ? new Date(date) : new Date;
+ if (isNaN(date)) throw SyntaxError("invalid date");
- // Passing date through Date applies Date.parse, if necessary
- date = date ? new Date(date) : new Date;
- if (isNaN(date)) throw SyntaxError("invalid date");
+ mask = String(dF.masks[mask] || mask || dF.masks["default"]);
- mask = String(dF.masks[mask] || mask || dF.masks["default"]);
+ // Allow setting the utc argument via the mask
+ if (mask.slice(0, 4) == "UTC:") {
+ mask = mask.slice(4);
+ utc = true;
+ }
- // Allow setting the utc argument via the mask
- if (mask.slice(0, 4) == "UTC:") {
- mask = mask.slice(4);
- utc = true;
- }
+ var _ = utc ? "getUTC" : "get",
+ d = date[_ + "Date"](),
+ D = date[_ + "Day"](),
+ m = date[_ + "Month"](),
+ y = date[_ + "FullYear"](),
+ H = date[_ + "Hours"](),
+ M = date[_ + "Minutes"](),
+ s = date[_ + "Seconds"](),
+ L = date[_ + "Milliseconds"](),
+ o = utc ? 0 : date.getTimezoneOffset(),
+ flags = {
+ d: d,
+ dd: pad(d),
+ ddd: dF.i18n.dayNames[D],
+ dddd: dF.i18n.dayNames[D + 7],
+ m: m + 1,
+ mm: pad(m + 1),
+ mmm: dF.i18n.monthNames[m],
+ mmmm: dF.i18n.monthNames[m + 12],
+ yy: String(y).slice(2),
+ yyyy: y,
+ h: H % 12 || 12,
+ hh: pad(H % 12 || 12),
+ H: H,
+ HH: pad(H),
+ M: M,
+ MM: pad(M),
+ s: s,
+ ss: pad(s),
+ l: pad(L, 3),
+ L: pad(L > 99 ? Math.round(L / 10) : L),
+ t: H < 12 ? "a" : "p",
+ tt: H < 12 ? "am" : "pm",
+ T: H < 12 ? "A" : "P",
+ TT: H < 12 ? "AM" : "PM",
+ Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
+ o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
+ S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
+ };
- var _ = utc ? "getUTC" : "get",
- d = date[_ + "Date"](),
- D = date[_ + "Day"](),
- m = date[_ + "Month"](),
- y = date[_ + "FullYear"](),
- H = date[_ + "Hours"](),
- M = date[_ + "Minutes"](),
- s = date[_ + "Seconds"](),
- L = date[_ + "Milliseconds"](),
- o = utc ? 0 : date.getTimezoneOffset(),
- flags = {
- d: d,
- dd: pad(d),
- ddd: dF.i18n.dayNames[D],
- dddd: dF.i18n.dayNames[D + 7],
- m: m + 1,
- mm: pad(m + 1),
- mmm: dF.i18n.monthNames[m],
- mmmm: dF.i18n.monthNames[m + 12],
- yy: String(y).slice(2),
- yyyy: y,
- h: H % 12 || 12,
- hh: pad(H % 12 || 12),
- H: H,
- HH: pad(H),
- M: M,
- MM: pad(M),
- s: s,
- ss: pad(s),
- l: pad(L, 3),
- L: pad(L > 99 ? Math.round(L / 10) : L),
- t: H < 12 ? "a" : "p",
- tt: H < 12 ? "am" : "pm",
- T: H < 12 ? "A" : "P",
- TT: H < 12 ? "AM" : "PM",
- Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
- o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
- S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
- };
+ return mask.replace(token, function ($0) {
+ return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
+ });
+ };
+ }();
- return mask.replace(token, function ($0) {
- return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
- });
+ // Some common format strings
+ dateFormat.masks = {
+ "default": "ddd mmm dd yyyy HH:MM:ss",
+ shortDate: "m/d/yy",
+ mediumDate: "mmm d, yyyy",
+ longDate: "mmmm d, yyyy",
+ fullDate: "dddd, mmmm d, yyyy",
+ shortTime: "h:MM TT",
+ mediumTime: "h:MM:ss TT",
+ longTime: "h:MM:ss TT Z",
+ isoDate: "yyyy-mm-dd",
+ isoTime: "HH:MM:ss",
+ isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
+ isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
};
-}();
-// Some common format strings
-dateFormat.masks = {
- "default": "ddd mmm dd yyyy HH:MM:ss",
- shortDate: "m/d/yy",
- mediumDate: "mmm d, yyyy",
- longDate: "mmmm d, yyyy",
- fullDate: "dddd, mmmm d, yyyy",
- shortTime: "h:MM TT",
- mediumTime: "h:MM:ss TT",
- longTime: "h:MM:ss TT Z",
- isoDate: "yyyy-mm-dd",
- isoTime: "HH:MM:ss",
- isoDateTime: "yyyy-mm-dd'T'HH:MM:ss",
- isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
-};
+ // Internationalization strings
+ dateFormat.i18n = {
+ dayNames: [
+ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+ ],
+ monthNames: [
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
+ ]
+ };
-// Internationalization strings
-dateFormat.i18n = {
- dayNames: [
- "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
- "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
- ],
- monthNames: [
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
- "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
- ]
-};
+ // For convenience...
+ Date.prototype.format = function (mask, utc) {
+ return dateFormat(this, mask, utc);
+ };
-// For convenience...
-Date.prototype.format = function (mask, utc) {
- return dateFormat(this, mask, utc);
-};
+ return dateFormat;
+})));
diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js
index 19e6c13a655..b8887115a37 100644
--- a/vendor/assets/javascripts/es6-promise.auto.js
+++ b/vendor/assets/javascripts/es6-promise.auto.js
@@ -1154,6 +1154,3 @@ Promise.Promise = Promise;
return Promise;
})));
-
-ES6Promise.polyfill();
-//# sourceMappingURL=es6-promise.auto.map
diff --git a/vendor/assets/javascripts/jquery.atwho.js b/vendor/assets/javascripts/jquery.atwho.js
new file mode 100644
index 00000000000..0d295ebe5af
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.atwho.js
@@ -0,0 +1,1202 @@
+/**
+ * at.js - 1.5.1
+ * Copyright (c) 2016 chord.luo <chord.luo@gmail.com>;
+ * Homepage: http://ichord.github.com/At.js
+ * License: MIT
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+var DEFAULT_CALLBACKS, KEY_CODE;
+
+KEY_CODE = {
+ DOWN: 40,
+ UP: 38,
+ ESC: 27,
+ TAB: 9,
+ ENTER: 13,
+ CTRL: 17,
+ A: 65,
+ P: 80,
+ N: 78,
+ LEFT: 37,
+ UP: 38,
+ RIGHT: 39,
+ DOWN: 40,
+ BACKSPACE: 8,
+ SPACE: 32
+};
+
+DEFAULT_CALLBACKS = {
+ beforeSave: function(data) {
+ return Controller.arrayToDefaultHash(data);
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var _a, _y, match, regexp, space;
+ flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
+ if (should_startWithSpace) {
+ flag = '(?:^|\\s)' + flag;
+ }
+ _a = decodeURI("%C3%80");
+ _y = decodeURI("%C3%BF");
+ space = acceptSpaceBar ? "\ " : "";
+ regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
+ match = regexp.exec(subtext);
+ if (match) {
+ return match[2] || match[1];
+ } else {
+ return null;
+ }
+ },
+ filter: function(query, data, searchKey) {
+ var _results, i, item, len;
+ _results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
+ _results.push(item);
+ }
+ }
+ return _results;
+ },
+ remoteFilter: null,
+ sorter: function(query, items, searchKey) {
+ var _results, i, item, len;
+ if (!query) {
+ return items;
+ }
+ _results = [];
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
+ if (item.atwho_order > -1) {
+ _results.push(item);
+ }
+ }
+ return _results.sort(function(a, b) {
+ return a.atwho_order - b.atwho_order;
+ });
+ },
+ tplEval: function(tpl, map) {
+ var error, error1, template;
+ template = tpl;
+ try {
+ if (typeof tpl !== 'string') {
+ template = tpl(map);
+ }
+ return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+ return map[key];
+ });
+ } catch (error1) {
+ error = error1;
+ return "";
+ }
+ },
+ highlighter: function(li, query) {
+ var regexp;
+ if (!query) {
+ return li;
+ }
+ regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
+ return li.replace(regexp, function(str, $1, $2, $3) {
+ return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+ });
+ },
+ beforeInsert: function(value, $li, e) {
+ return value;
+ },
+ beforeReposition: function(offset) {
+ return offset;
+ },
+ afterMatchFailed: function(at, el) {}
+};
+
+var App;
+
+App = (function() {
+ function App(inputor) {
+ this.currentFlag = null;
+ this.controllers = {};
+ this.aliasMaps = {};
+ this.$inputor = $(inputor);
+ this.setupRootElement();
+ this.listen();
+ }
+
+ App.prototype.createContainer = function(doc) {
+ var ref;
+ if ((ref = this.$el) != null) {
+ ref.remove();
+ }
+ return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>"));
+ };
+
+ App.prototype.setupRootElement = function(iframe, asRoot) {
+ var error, error1;
+ if (asRoot == null) {
+ asRoot = false;
+ }
+ if (iframe) {
+ this.window = iframe.contentWindow;
+ this.document = iframe.contentDocument || this.window.document;
+ this.iframe = iframe;
+ } else {
+ this.document = this.$inputor[0].ownerDocument;
+ this.window = this.document.defaultView || this.document.parentWindow;
+ try {
+ this.iframe = this.window.frameElement;
+ } catch (error1) {
+ error = error1;
+ this.iframe = null;
+ if ($.fn.atwho.debug) {
+ throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
+ }
+ }
+ }
+ return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
+ };
+
+ App.prototype.controller = function(at) {
+ var c, current, currentFlag, ref;
+ if (this.aliasMaps[at]) {
+ current = this.controllers[this.aliasMaps[at]];
+ } else {
+ ref = this.controllers;
+ for (currentFlag in ref) {
+ c = ref[currentFlag];
+ if (currentFlag === at) {
+ current = c;
+ break;
+ }
+ }
+ }
+ if (current) {
+ return current;
+ } else {
+ return this.controllers[this.currentFlag];
+ }
+ };
+
+ App.prototype.setContextFor = function(at) {
+ this.currentFlag = at;
+ return this;
+ };
+
+ App.prototype.reg = function(flag, setting) {
+ var base, controller;
+ controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
+ if (setting.alias) {
+ this.aliasMaps[setting.alias] = flag;
+ }
+ controller.init(setting);
+ return this;
+ };
+
+ App.prototype.listen = function() {
+ return this.$inputor.on('compositionstart', (function(_this) {
+ return function(e) {
+ var ref;
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide();
+ }
+ _this.isComposing = true;
+ return null;
+ };
+ })(this)).on('compositionend', (function(_this) {
+ return function(e) {
+ _this.isComposing = false;
+ setTimeout(function(e) {
+ return _this.dispatch(e);
+ });
+ return null;
+ };
+ })(this)).on('keyup.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeyup(e);
+ };
+ })(this)).on('keydown.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.onKeydown(e);
+ };
+ })(this)).on('blur.atwhoInner', (function(_this) {
+ return function(e) {
+ var c;
+ if (c = _this.controller()) {
+ c.expectedQueryCBId = null;
+ return c.view.hide(e, c.getOpt("displayTimeout"));
+ }
+ };
+ })(this)).on('click.atwhoInner', (function(_this) {
+ return function(e) {
+ return _this.dispatch(e);
+ };
+ })(this)).on('scroll.atwhoInner', (function(_this) {
+ return function() {
+ var lastScrollTop;
+ lastScrollTop = _this.$inputor.scrollTop();
+ return function(e) {
+ var currentScrollTop, ref;
+ currentScrollTop = e.target.scrollTop;
+ if (lastScrollTop !== currentScrollTop) {
+ if ((ref = _this.controller()) != null) {
+ ref.view.hide(e);
+ }
+ }
+ lastScrollTop = currentScrollTop;
+ return true;
+ };
+ };
+ })(this)());
+ };
+
+ App.prototype.shutdown = function() {
+ var _, c, ref;
+ ref = this.controllers;
+ for (_ in ref) {
+ c = ref[_];
+ c.destroy();
+ delete this.controllers[_];
+ }
+ this.$inputor.off('.atwhoInner');
+ return this.$el.remove();
+ };
+
+ App.prototype.dispatch = function(e) {
+ var _, c, ref, results;
+ ref = this.controllers;
+ results = [];
+ for (_ in ref) {
+ c = ref[_];
+ results.push(c.lookUp(e));
+ }
+ return results;
+ };
+
+ App.prototype.onKeyup = function(e) {
+ var ref;
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ if ((ref = this.controller()) != null) {
+ ref.view.hide();
+ }
+ break;
+ case KEY_CODE.DOWN:
+ case KEY_CODE.UP:
+ case KEY_CODE.CTRL:
+ case KEY_CODE.ENTER:
+ $.noop();
+ break;
+ case KEY_CODE.P:
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ this.dispatch(e);
+ }
+ break;
+ default:
+ this.dispatch(e);
+ }
+ };
+
+ App.prototype.onKeydown = function(e) {
+ var ref, view;
+ view = (ref = this.controller()) != null ? ref.view : void 0;
+ if (!(view && view.visible())) {
+ return;
+ }
+ switch (e.keyCode) {
+ case KEY_CODE.ESC:
+ e.preventDefault();
+ view.hide(e);
+ break;
+ case KEY_CODE.UP:
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.DOWN:
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.P:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.prev();
+ break;
+ case KEY_CODE.N:
+ if (!e.ctrlKey) {
+ return;
+ }
+ e.preventDefault();
+ view.next();
+ break;
+ case KEY_CODE.TAB:
+ case KEY_CODE.ENTER:
+ case KEY_CODE.SPACE:
+ if (!view.visible()) {
+ return;
+ }
+ if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
+ return;
+ }
+ if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
+ return;
+ }
+ if (view.highlighted()) {
+ e.preventDefault();
+ view.choose(e);
+ } else {
+ view.hide(e);
+ }
+ break;
+ default:
+ $.noop();
+ }
+ };
+
+ return App;
+
+})();
+
+var Controller,
+ slice = [].slice;
+
+Controller = (function() {
+ Controller.prototype.uid = function() {
+ return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
+ };
+
+ function Controller(app, at1) {
+ this.app = app;
+ this.at = at1;
+ this.$inputor = this.app.$inputor;
+ this.id = this.$inputor[0].id || this.uid();
+ this.expectedQueryCBId = null;
+ this.setting = null;
+ this.query = null;
+ this.pos = 0;
+ this.range = null;
+ if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
+ this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
+ }
+ this.model = new Model(this);
+ this.view = new View(this);
+ }
+
+ Controller.prototype.init = function(setting) {
+ this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
+ this.view.init();
+ return this.model.reload(this.setting.data);
+ };
+
+ Controller.prototype.destroy = function() {
+ this.trigger('beforeDestroy');
+ this.model.destroy();
+ this.view.destroy();
+ return this.$el.remove();
+ };
+
+ Controller.prototype.callDefault = function() {
+ var args, error, error1, funcName;
+ funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
+ try {
+ return DEFAULT_CALLBACKS[funcName].apply(this, args);
+ } catch (error1) {
+ error = error1;
+ return $.error(error + " Or maybe At.js doesn't have function " + funcName);
+ }
+ };
+
+ Controller.prototype.trigger = function(name, data) {
+ var alias, eventName;
+ if (data == null) {
+ data = [];
+ }
+ data.push(this);
+ alias = this.getOpt('alias');
+ eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
+ return this.$inputor.trigger(eventName, data);
+ };
+
+ Controller.prototype.callbacks = function(funcName) {
+ return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
+ };
+
+ Controller.prototype.getOpt = function(at, default_value) {
+ var e, error1;
+ try {
+ return this.setting[at];
+ } catch (error1) {
+ e = error1;
+ return null;
+ }
+ };
+
+ Controller.prototype.insertContentFor = function($li) {
+ var data, tpl;
+ tpl = this.getOpt('insertTpl');
+ data = $.extend({}, $li.data('item-data'), {
+ 'atwho-at': this.at
+ });
+ return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
+ };
+
+ Controller.prototype.renderView = function(data) {
+ var searchKey;
+ searchKey = this.getOpt("searchKey");
+ data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
+ return this.view.render(data.slice(0, this.getOpt('limit')));
+ };
+
+ Controller.arrayToDefaultHash = function(data) {
+ var i, item, len, results;
+ if (!$.isArray(data)) {
+ return data;
+ }
+ results = [];
+ for (i = 0, len = data.length; i < len; i++) {
+ item = data[i];
+ if ($.isPlainObject(item)) {
+ results.push(item);
+ } else {
+ results.push({
+ name: item
+ });
+ }
+ }
+ return results;
+ };
+
+ Controller.prototype.lookUp = function(e) {
+ var query, wait;
+ if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
+ return;
+ }
+ if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
+ return;
+ }
+ query = this.catchQuery(e);
+ if (!query) {
+ this.expectedQueryCBId = null;
+ return query;
+ }
+ this.app.setContextFor(this.at);
+ if (wait = this.getOpt('delay')) {
+ this._delayLookUp(query, wait);
+ } else {
+ this._lookUp(query);
+ }
+ return query;
+ };
+
+ Controller.prototype._delayLookUp = function(query, wait) {
+ var now, remaining;
+ now = Date.now ? Date.now() : new Date().getTime();
+ this.previousCallTime || (this.previousCallTime = now);
+ remaining = wait - (now - this.previousCallTime);
+ if ((0 < remaining && remaining < wait)) {
+ this.previousCallTime = now;
+ this._stopDelayedCall();
+ return this.delayedCallTimeout = setTimeout((function(_this) {
+ return function() {
+ _this.previousCallTime = 0;
+ _this.delayedCallTimeout = null;
+ return _this._lookUp(query);
+ };
+ })(this), wait);
+ } else {
+ this._stopDelayedCall();
+ if (this.previousCallTime !== now) {
+ this.previousCallTime = 0;
+ }
+ return this._lookUp(query);
+ }
+ };
+
+ Controller.prototype._stopDelayedCall = function() {
+ if (this.delayedCallTimeout) {
+ clearTimeout(this.delayedCallTimeout);
+ return this.delayedCallTimeout = null;
+ }
+ };
+
+ Controller.prototype._generateQueryCBId = function() {
+ return {};
+ };
+
+ Controller.prototype._lookUp = function(query) {
+ var _callback;
+ _callback = function(queryCBId, data) {
+ if (queryCBId !== this.expectedQueryCBId) {
+ return;
+ }
+ if (data && data.length > 0) {
+ return this.renderView(this.constructor.arrayToDefaultHash(data));
+ } else {
+ return this.view.hide();
+ }
+ };
+ this.expectedQueryCBId = this._generateQueryCBId();
+ return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
+ };
+
+ return Controller;
+
+})();
+
+var TextareaController,
+ extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+TextareaController = (function(superClass) {
+ extend(TextareaController, superClass);
+
+ function TextareaController() {
+ return TextareaController.__super__.constructor.apply(this, arguments);
+ }
+
+ TextareaController.prototype.catchQuery = function() {
+ var caretPos, content, end, isString, query, start, subtext;
+ content = this.$inputor.val();
+ caretPos = this.$inputor.caret('pos', {
+ iframe: this.app.iframe
+ });
+ subtext = content.slice(0, caretPos);
+ query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof query === 'string';
+ if (isString && query.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && query.length <= this.getOpt('maxLen', 20)) {
+ start = caretPos - query.length;
+ end = start + query.length;
+ this.pos = start;
+ query = {
+ 'text': query,
+ 'headPos': start,
+ 'endPos': end
+ };
+ this.trigger("matched", [this.at, query.text]);
+ } else {
+ query = null;
+ this.view.hide();
+ }
+ return this.query = query;
+ };
+
+ TextareaController.prototype.rect = function() {
+ var c, iframeOffset, scaleBottom;
+ if (!(c = this.$inputor.caret('offset', this.pos - 1, {
+ iframe: this.app.iframe
+ }))) {
+ return;
+ }
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = $(this.app.iframe).offset();
+ c.left += iframeOffset.left;
+ c.top += iframeOffset.top;
+ }
+ scaleBottom = this.app.document.selection ? 0 : 2;
+ return {
+ left: c.left,
+ top: c.top,
+ bottom: c.top + c.height + scaleBottom
+ };
+ };
+
+ TextareaController.prototype.insert = function(content, $li) {
+ var $inputor, source, startStr, suffix, text;
+ $inputor = this.$inputor;
+ source = $inputor.val();
+ startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
+ content += suffix;
+ text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
+ $inputor.val(text);
+ $inputor.caret('pos', startStr.length + content.length, {
+ iframe: this.app.iframe
+ });
+ if (!$inputor.is(':focus')) {
+ $inputor.focus();
+ }
+ return $inputor.change();
+ };
+
+ return TextareaController;
+
+})(Controller);
+
+var EditableController,
+ extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+EditableController = (function(superClass) {
+ extend(EditableController, superClass);
+
+ function EditableController() {
+ return EditableController.__super__.constructor.apply(this, arguments);
+ }
+
+ EditableController.prototype._getRange = function() {
+ var sel;
+ sel = this.app.window.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ }
+ };
+
+ EditableController.prototype._setRange = function(position, node, range) {
+ if (range == null) {
+ range = this._getRange();
+ }
+ if (!range) {
+ return;
+ }
+ node = $(node)[0];
+ if (position === 'after') {
+ range.setEndAfter(node);
+ range.setStartAfter(node);
+ } else {
+ range.setEndBefore(node);
+ range.setStartBefore(node);
+ }
+ range.collapse(false);
+ return this._clearRange(range);
+ };
+
+ EditableController.prototype._clearRange = function(range) {
+ var sel;
+ if (range == null) {
+ range = this._getRange();
+ }
+ sel = this.app.window.getSelection();
+ if (this.ctrl_a_pressed == null) {
+ sel.removeAllRanges();
+ return sel.addRange(range);
+ }
+ };
+
+ EditableController.prototype._movingEvent = function(e) {
+ var ref;
+ return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
+ };
+
+ EditableController.prototype._unwrap = function(node) {
+ var next;
+ node = $(node).unwrap().get(0);
+ if ((next = node.nextSibling) && next.nodeValue) {
+ node.nodeValue += next.nodeValue;
+ $(next).remove();
+ }
+ return node;
+ };
+
+ EditableController.prototype.catchQuery = function(e) {
+ var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
+ if (!(range = this._getRange())) {
+ return;
+ }
+ if (!range.collapsed) {
+ return;
+ }
+ if (e.which === KEY_CODE.ENTER) {
+ ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
+ if ($query.is(':empty')) {
+ $query.remove();
+ }
+ ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
+ this._clearRange();
+ return;
+ }
+ if (/firefox/i.test(navigator.userAgent)) {
+ if ($(range.startContainer).is(this.$inputor)) {
+ this._clearRange();
+ return;
+ }
+ if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, offset);
+ if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
+ inserted = $(range.startContainer).contents().get(offset);
+ this._setRange('after', $(inserted).contents().last());
+ }
+ } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
+ $inserted = $(range.startContainer.previousSibling);
+ if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
+ this._setRange('after', $inserted.contents().last());
+ }
+ }
+ }
+ $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
+ if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
+ $query.remove();
+ }
+ if (!this._movingEvent(e)) {
+ $query.removeClass('atwho-inserted');
+ }
+ if ($query.length > 0) {
+ switch (e.which) {
+ case KEY_CODE.LEFT:
+ this._setRange('before', $query.get(0), range);
+ $query.removeClass('atwho-query');
+ return;
+ case KEY_CODE.RIGHT:
+ this._setRange('after', $query.get(0).nextSibling, range);
+ $query.removeClass('atwho-query');
+ return;
+ }
+ }
+ if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
+ $query.empty().html(query_content).attr('data-atwho-at-query', null);
+ this._setRange('after', $query.get(0), range);
+ }
+ _range = range.cloneRange();
+ _range.setStart(range.startContainer, 0);
+ matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
+ isString = typeof matched === 'string';
+ if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
+ range.setStart(range.startContainer, index);
+ $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
+ range.surroundContents($query.get(0));
+ lastNode = $query.contents().last().get(0);
+ if (/firefox/i.test(navigator.userAgent)) {
+ range.setStart(lastNode, lastNode.length);
+ range.setEnd(lastNode, lastNode.length);
+ this._clearRange(range);
+ } else {
+ this._setRange('after', lastNode, range);
+ }
+ }
+ if (isString && matched.length < this.getOpt('minLen', 0)) {
+ return;
+ }
+ if (isString && matched.length <= this.getOpt('maxLen', 20)) {
+ query = {
+ text: matched,
+ el: $query
+ };
+ this.trigger("matched", [this.at, query.text]);
+ return this.query = query;
+ } else {
+ this.view.hide();
+ this.query = {
+ el: $query
+ };
+ if ($query.text().indexOf(this.at) >= 0) {
+ if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
+ $query.removeClass('atwho-query');
+ } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
+ this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
+ }
+ }
+ return null;
+ }
+ };
+
+ EditableController.prototype.rect = function() {
+ var $iframe, iframeOffset, rect;
+ rect = this.query.el.offset();
+ if (this.app.iframe && !this.app.iframeAsRoot) {
+ iframeOffset = ($iframe = $(this.app.iframe)).offset();
+ rect.left += iframeOffset.left - this.$inputor.scrollLeft();
+ rect.top += iframeOffset.top - this.$inputor.scrollTop();
+ }
+ rect.bottom = rect.top + this.query.el.height();
+ return rect;
+ };
+
+ EditableController.prototype.insert = function(content, $li) {
+ var data, range, suffix, suffixNode;
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
+ data = $li.data('item-data');
+ this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text);
+ if (range = this._getRange()) {
+ range.setEndAfter(this.query.el[0]);
+ range.collapse(false);
+ range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix));
+ this._setRange('after', suffixNode, range);
+ }
+ if (!this.$inputor.is(':focus')) {
+ this.$inputor.focus();
+ }
+ return this.$inputor.change();
+ };
+
+ return EditableController;
+
+})(Controller);
+
+var Model;
+
+Model = (function() {
+ function Model(context) {
+ this.context = context;
+ this.at = this.context.at;
+ this.storage = this.context.$inputor;
+ }
+
+ Model.prototype.destroy = function() {
+ return this.storage.data(this.at, null);
+ };
+
+ Model.prototype.saved = function() {
+ return this.fetch() > 0;
+ };
+
+ Model.prototype.query = function(query, callback) {
+ var _remoteFilter, data, searchKey;
+ data = this.fetch();
+ searchKey = this.context.getOpt("searchKey");
+ data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
+ _remoteFilter = this.context.callbacks('remoteFilter');
+ if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
+ return callback(data);
+ } else {
+ return _remoteFilter.call(this.context, query, callback);
+ }
+ };
+
+ Model.prototype.fetch = function() {
+ return this.storage.data(this.at) || [];
+ };
+
+ Model.prototype.save = function(data) {
+ return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
+ };
+
+ Model.prototype.load = function(data) {
+ if (!(this.saved() || !data)) {
+ return this._load(data);
+ }
+ };
+
+ Model.prototype.reload = function(data) {
+ return this._load(data);
+ };
+
+ Model.prototype._load = function(data) {
+ if (typeof data === "string") {
+ return $.ajax(data, {
+ dataType: "json"
+ }).done((function(_this) {
+ return function(data) {
+ return _this.save(data);
+ };
+ })(this));
+ } else {
+ return this.save(data);
+ }
+ };
+
+ return Model;
+
+})();
+
+var View;
+
+View = (function() {
+ function View(context) {
+ this.context = context;
+ this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
+ this.$elUl = this.$el.children();
+ this.timeoutID = null;
+ this.context.$el.append(this.$el);
+ this.bindEvent();
+ }
+
+ View.prototype.init = function() {
+ var header_tpl, id;
+ id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
+ header_tpl = this.context.getOpt("headerTpl");
+ if (header_tpl && this.$el.children().length === 1) {
+ this.$el.prepend(header_tpl);
+ }
+ return this.$el.attr({
+ 'id': "at-view-" + id
+ });
+ };
+
+ View.prototype.destroy = function() {
+ return this.$el.remove();
+ };
+
+ View.prototype.bindEvent = function() {
+ var $menu, lastCoordX, lastCoordY;
+ $menu = this.$el.find('ul');
+ lastCoordX = 0;
+ lastCoordY = 0;
+ return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ var $cur;
+ if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
+ return;
+ }
+ lastCoordX = e.clientX;
+ lastCoordY = e.clientY;
+ $cur = $(e.currentTarget);
+ if ($cur.hasClass('cur')) {
+ return;
+ }
+ $menu.find('.cur').removeClass('cur');
+ return $cur.addClass('cur');
+ };
+ })(this)).on('click.atwho-view', 'li', (function(_this) {
+ return function(e) {
+ $menu.find('.cur').removeClass('cur');
+ $(e.currentTarget).addClass('cur');
+ _this.choose(e);
+ return e.preventDefault();
+ };
+ })(this));
+ };
+
+ View.prototype.visible = function() {
+ return this.$el.is(":visible");
+ };
+
+ View.prototype.highlighted = function() {
+ return this.$el.find(".cur").length > 0;
+ };
+
+ View.prototype.choose = function(e) {
+ var $li, content;
+ if (($li = this.$el.find(".cur")).length) {
+ content = this.context.insertContentFor($li);
+ this.context._stopDelayedCall();
+ this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
+ this.context.trigger("inserted", [$li, e]);
+ this.hide(e);
+ }
+ if (this.context.getOpt("hideWithoutSuffix")) {
+ return this.stopShowing = true;
+ }
+ };
+
+ View.prototype.reposition = function(rect) {
+ var _window, offset, overflowOffset, ref;
+ _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
+ if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
+ rect.bottom = rect.top - this.$el.height();
+ }
+ if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
+ rect.left = overflowOffset;
+ }
+ offset = {
+ left: rect.left,
+ top: rect.bottom
+ };
+ if ((ref = this.context.callbacks("beforeReposition")) != null) {
+ ref.call(this.context, offset);
+ }
+ this.$el.offset(offset);
+ return this.context.trigger("reposition", [offset]);
+ };
+
+ View.prototype.next = function() {
+ var cur, next, nextEl, offset;
+ cur = this.$el.find('.cur').removeClass('cur');
+ next = cur.next();
+ if (!next.length) {
+ next = this.$el.find('li:first');
+ }
+ next.addClass('cur');
+ nextEl = next[0];
+ offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.prev = function() {
+ var cur, offset, prev, prevEl;
+ cur = this.$el.find('.cur').removeClass('cur');
+ prev = cur.prev();
+ if (!prev.length) {
+ prev = this.$el.find('li:last');
+ }
+ prev.addClass('cur');
+ prevEl = prev[0];
+ offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
+ return this.scrollTop(Math.max(0, offset - this.$el.height()));
+ };
+
+ View.prototype.scrollTop = function(scrollTop) {
+ var scrollDuration;
+ scrollDuration = this.context.getOpt('scrollDuration');
+ if (scrollDuration) {
+ return this.$elUl.animate({
+ scrollTop: scrollTop
+ }, scrollDuration);
+ } else {
+ return this.$elUl.scrollTop(scrollTop);
+ }
+ };
+
+ View.prototype.show = function() {
+ var rect;
+ if (this.stopShowing) {
+ this.stopShowing = false;
+ return;
+ }
+ if (!this.visible()) {
+ this.$el.show();
+ this.$el.scrollTop(0);
+ this.context.trigger('shown');
+ }
+ if (rect = this.context.rect()) {
+ return this.reposition(rect);
+ }
+ };
+
+ View.prototype.hide = function(e, time) {
+ var callback;
+ if (!this.visible()) {
+ return;
+ }
+ if (isNaN(time)) {
+ this.$el.hide();
+ return this.context.trigger('hidden', [e]);
+ } else {
+ callback = (function(_this) {
+ return function() {
+ return _this.hide();
+ };
+ })(this);
+ clearTimeout(this.timeoutID);
+ return this.timeoutID = setTimeout(callback, time);
+ }
+ };
+
+ View.prototype.render = function(list) {
+ var $li, $ul, i, item, len, li, tpl;
+ if (!($.isArray(list) && list.length > 0)) {
+ this.hide();
+ return;
+ }
+ this.$el.find('ul').empty();
+ $ul = this.$el.find('ul');
+ tpl = this.context.getOpt('displayTpl');
+ for (i = 0, len = list.length; i < len; i++) {
+ item = list[i];
+ item = $.extend({}, item, {
+ 'atwho-at': this.context.at
+ });
+ li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
+ $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
+ $li.data("item-data", item);
+ $ul.append($li);
+ }
+ this.show();
+ if (this.context.getOpt('highlightFirst')) {
+ return $ul.find("li:first").addClass("cur");
+ }
+ };
+
+ return View;
+
+})();
+
+var Api;
+
+Api = {
+ load: function(at, data) {
+ var c;
+ if (c = this.controller(at)) {
+ return c.model.load(data);
+ }
+ },
+ isSelecting: function() {
+ var ref;
+ return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
+ },
+ hide: function() {
+ var ref;
+ return (ref = this.controller()) != null ? ref.view.hide() : void 0;
+ },
+ reposition: function() {
+ var c;
+ if (c = this.controller()) {
+ return c.view.reposition(c.rect());
+ }
+ },
+ setIframe: function(iframe, asRoot) {
+ this.setupRootElement(iframe, asRoot);
+ return null;
+ },
+ run: function() {
+ return this.dispatch();
+ },
+ destroy: function() {
+ this.shutdown();
+ return this.$inputor.data('atwho', null);
+ }
+};
+
+$.fn.atwho = function(method) {
+ var _args, result;
+ _args = arguments;
+ result = null;
+ this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
+ var $this, app;
+ if (!(app = ($this = $(this)).data("atwho"))) {
+ $this.data('atwho', (app = new App(this)));
+ }
+ if (typeof method === 'object' || !method) {
+ return app.reg(method.at, method);
+ } else if (Api[method] && app) {
+ return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.atwho");
+ }
+ });
+ if (result != null) {
+ return result;
+ } else {
+ return this;
+ }
+};
+
+$.fn.atwho["default"] = {
+ at: void 0,
+ alias: void 0,
+ data: null,
+ displayTpl: "<li>${name}</li>",
+ insertTpl: "${atwho-at}${name}",
+ headerTpl: null,
+ callbacks: DEFAULT_CALLBACKS,
+ searchKey: "name",
+ suffix: void 0,
+ hideWithoutSuffix: false,
+ startWithSpace: true,
+ acceptSpaceBar: false,
+ highlightFirst: true,
+ limit: 5,
+ maxLen: 20,
+ minLen: 0,
+ displayTimeout: 300,
+ delay: null,
+ spaceSelectsMatch: false,
+ tabSelectsMatch: true,
+ editableAtwhoQueryAttrs: {},
+ scrollDuration: 150,
+ suspendOnComposing: true,
+ lookUpOnClick: true
+};
+
+$.fn.atwho.debug = false;
+
+}));
diff --git a/vendor/assets/javascripts/jquery.ba-resize.js b/vendor/assets/javascripts/jquery.ba-resize.js
deleted file mode 100644
index 1f41d379153..00000000000
--- a/vendor/assets/javascripts/jquery.ba-resize.js
+++ /dev/null
@@ -1,246 +0,0 @@
-/*!
- * jQuery resize event - v1.1 - 3/14/2010
- * http://benalman.com/projects/jquery-resize-plugin/
- *
- * Copyright (c) 2010 "Cowboy" Ben Alman
- * Dual licensed under the MIT and GPL licenses.
- * http://benalman.com/about/license/
- */
-
-// Script: jQuery resize event
-//
-// *Version: 1.1, Last updated: 3/14/2010*
-//
-// Project Home - http://benalman.com/projects/jquery-resize-plugin/
-// GitHub - http://github.com/cowboy/jquery-resize/
-// Source - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.js
-// (Minified) - http://github.com/cowboy/jquery-resize/raw/master/jquery.ba-resize.min.js (1.0kb)
-//
-// About: License
-//
-// Copyright (c) 2010 "Cowboy" Ben Alman,
-// Dual licensed under the MIT and GPL licenses.
-// http://benalman.com/about/license/
-//
-// About: Examples
-//
-// This working example, complete with fully commented code, illustrates a few
-// ways in which this plugin can be used.
-//
-// resize event - http://benalman.com/code/projects/jquery-resize/examples/resize/
-//
-// About: Support and Testing
-//
-// Information about what version or versions of jQuery this plugin has been
-// tested with, what browsers it has been tested in, and where the unit tests
-// reside (so you can test it yourself).
-//
-// jQuery Versions - 1.3.2, 1.4.1, 1.4.2
-// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1.
-// Unit Tests - http://benalman.com/code/projects/jquery-resize/unit/
-//
-// About: Release History
-//
-// 1.1 - (3/14/2010) Fixed a minor bug that was causing the event to trigger
-// immediately after bind in some circumstances. Also changed $.fn.data
-// to $.data to improve performance.
-// 1.0 - (2/10/2010) Initial release
-
-(function($,window,undefined){
- '$:nomunge'; // Used by YUI compressor.
-
- // A jQuery object containing all non-window elements to which the resize
- // event is bound.
- var elems = $([]),
-
- // Extend $.resize if it already exists, otherwise create it.
- jq_resize = $.resize = $.extend( $.resize, {} ),
-
- timeout_id,
-
- // Reused strings.
- str_setTimeout = 'setTimeout',
- str_resize = 'resize',
- str_data = str_resize + '-special-event',
- str_delay = 'delay',
- str_throttle = 'throttleWindow';
-
- // Property: jQuery.resize.delay
- //
- // The numeric interval (in milliseconds) at which the resize event polling
- // loop executes. Defaults to 250.
-
- jq_resize[ str_delay ] = 250;
-
- // Property: jQuery.resize.throttleWindow
- //
- // Throttle the native window object resize event to fire no more than once
- // every <jQuery.resize.delay> milliseconds. Defaults to true.
- //
- // Because the window object has its own resize event, it doesn't need to be
- // provided by this plugin, and its execution can be left entirely up to the
- // browser. However, since certain browsers fire the resize event continuously
- // while others do not, enabling this will throttle the window resize event,
- // making event behavior consistent across all elements in all browsers.
- //
- // While setting this property to false will disable window object resize
- // event throttling, please note that this property must be changed before any
- // window object resize event callbacks are bound.
-
- jq_resize[ str_throttle ] = true;
-
- // Event: resize event
- //
- // Fired when an element's width or height changes. Because browsers only
- // provide this event for the window element, for other elements a polling
- // loop is initialized, running every <jQuery.resize.delay> milliseconds
- // to see if elements' dimensions have changed. You may bind with either
- // .resize( fn ) or .bind( "resize", fn ), and unbind with .unbind( "resize" ).
- //
- // Usage:
- //
- // > jQuery('selector').bind( 'resize', function(e) {
- // > // element's width or height has changed!
- // > ...
- // > });
- //
- // Additional Notes:
- //
- // * The polling loop is not created until at least one callback is actually
- // bound to the 'resize' event, and this single polling loop is shared
- // across all elements.
- //
- // Double firing issue in jQuery 1.3.2:
- //
- // While this plugin works in jQuery 1.3.2, if an element's event callbacks
- // are manually triggered via .trigger( 'resize' ) or .resize() those
- // callbacks may double-fire, due to limitations in the jQuery 1.3.2 special
- // events system. This is not an issue when using jQuery 1.4+.
- //
- // > // While this works in jQuery 1.4+
- // > $(elem).css({ width: new_w, height: new_h }).resize();
- // >
- // > // In jQuery 1.3.2, you need to do this:
- // > var elem = $(elem);
- // > elem.css({ width: new_w, height: new_h });
- // > elem.data( 'resize-special-event', { width: elem.width(), height: elem.height() } );
- // > elem.resize();
-
- $.event.special[ str_resize ] = {
-
- // Called only when the first 'resize' event callback is bound per element.
- setup: function() {
- // Since window has its own native 'resize' event, return false so that
- // jQuery will bind the event using DOM methods. Since only 'window'
- // objects have a .setTimeout method, this should be a sufficient test.
- // Unless, of course, we're throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var elem = $(this);
-
- // Add this element to the list of internal elements to monitor.
- elems = elems.add( elem );
-
- // Initialize data store on the element.
- $.data( this, str_data, { w: elem.width(), h: elem.height() } );
-
- // If this is the first element added, start the polling loop.
- if ( elems.length === 1 ) {
- loopy();
- }
- },
-
- // Called only when the last 'resize' event callback is unbound per element.
- teardown: function() {
- // Since window has its own native 'resize' event, return false so that
- // jQuery will unbind the event using DOM methods. Since only 'window'
- // objects have a .setTimeout method, this should be a sufficient test.
- // Unless, of course, we're throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var elem = $(this);
-
- // Remove this element from the list of internal elements to monitor.
- elems = elems.not( elem );
-
- // Remove any data stored on the element.
- elem.removeData( str_data );
-
- // If this is the last element removed, stop the polling loop.
- if ( !elems.length ) {
- clearTimeout( timeout_id );
- }
- },
-
- // Called every time a 'resize' event callback is bound per element (new in
- // jQuery 1.4).
- add: function( handleObj ) {
- // Since window has its own native 'resize' event, return false so that
- // jQuery doesn't modify the event object. Unless, of course, we're
- // throttling the 'resize' event for window.
- if ( !jq_resize[ str_throttle ] && this[ str_setTimeout ] ) { return false; }
-
- var old_handler;
-
- // The new_handler function is executed every time the event is triggered.
- // This is used to update the internal element data store with the width
- // and height when the event is triggered manually, to avoid double-firing
- // of the event callback. See the "Double firing issue in jQuery 1.3.2"
- // comments above for more information.
-
- function new_handler( e, w, h ) {
- var elem = $(this),
- data = $.data( this, str_data );
-
- // If called from the polling loop, w and h will be passed in as
- // arguments. If called manually, via .trigger( 'resize' ) or .resize(),
- // those values will need to be computed.
- data.w = w !== undefined ? w : elem.width();
- data.h = h !== undefined ? h : elem.height();
-
- old_handler.apply( this, arguments );
- };
-
- // This may seem a little complicated, but it normalizes the special event
- // .add method between jQuery 1.4/1.4.1 and 1.4.2+
- if ( $.isFunction( handleObj ) ) {
- // 1.4, 1.4.1
- old_handler = handleObj;
- return new_handler;
- } else {
- // 1.4.2+
- old_handler = handleObj.handler;
- handleObj.handler = new_handler;
- }
- }
-
- };
-
- function loopy() {
-
- // Start the polling loop, asynchronously.
- timeout_id = window[ str_setTimeout ](function(){
-
- // Iterate over all elements to which the 'resize' event is bound.
- elems.each(function(){
- var elem = $(this),
- width = elem.width(),
- height = elem.height(),
- data = $.data( this, str_data );
-
- // If element size has changed since the last time, update the element
- // data store and trigger the 'resize' event.
- if ( width !== data.w || height !== data.h ) {
- elem.trigger( str_resize, [ data.w = width, data.h = height ] );
- }
-
- });
-
- // Loop.
- loopy();
-
- }, jq_resize[ str_delay ] );
-
- };
-
-})(jQuery,this);
diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js
new file mode 100644
index 00000000000..811ec63ee47
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.caret.js
@@ -0,0 +1,436 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(["jquery"], function ($) {
+ return (root.returnExportsGlobal = factory($));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like enviroments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+/*
+ Implement Github like autocomplete mentions
+ http://ichord.github.com/At.js
+
+ Copyright (c) 2013 chord.luo@gmail.com
+ Licensed under the MIT license.
+*/
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+"use strict";
+var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
+
+pluginName = 'caret';
+
+EditableCaret = (function() {
+ function EditableCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ EditableCaret.prototype.setPos = function(pos) {
+ var fn, found, offset, sel;
+ if (sel = oWindow.getSelection()) {
+ offset = 0;
+ found = false;
+ (fn = function(pos, parent) {
+ var node, range, _i, _len, _ref, _results;
+ _ref = parent.childNodes;
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ node = _ref[_i];
+ if (found) {
+ break;
+ }
+ if (node.nodeType === 3) {
+ if (offset + node.length >= pos) {
+ found = true;
+ range = oDocument.createRange();
+ range.setStart(node, pos - offset);
+ sel.removeAllRanges();
+ sel.addRange(range);
+ break;
+ } else {
+ _results.push(offset += node.length);
+ }
+ } else {
+ _results.push(fn(pos, node));
+ }
+ }
+ return _results;
+ })(pos, this.domInputor);
+ }
+ return this.domInputor;
+ };
+
+ EditableCaret.prototype.getIEPosition = function() {
+ return this.getPosition();
+ };
+
+ EditableCaret.prototype.getPosition = function() {
+ var inputor_offset, offset;
+ offset = this.getOffset();
+ inputor_offset = this.$inputor.offset();
+ offset.left -= inputor_offset.left;
+ offset.top -= inputor_offset.top;
+ return offset;
+ };
+
+ EditableCaret.prototype.getOldIEPos = function() {
+ var preCaretTextRange, textRange;
+ textRange = oDocument.selection.createRange();
+ preCaretTextRange = oDocument.body.createTextRange();
+ preCaretTextRange.moveToElementText(this.domInputor);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ return preCaretTextRange.text.length;
+ };
+
+ EditableCaret.prototype.getPos = function() {
+ var clonedRange, pos, range;
+ if (range = this.range()) {
+ clonedRange = range.cloneRange();
+ clonedRange.selectNodeContents(this.domInputor);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ pos = clonedRange.toString().length;
+ clonedRange.detach();
+ return pos;
+ } else if (oDocument.selection) {
+ return this.getOldIEPos();
+ }
+ };
+
+ EditableCaret.prototype.getOldIEOffset = function() {
+ var range, rect;
+ range = oDocument.selection.createRange().duplicate();
+ range.moveStart("character", -1);
+ rect = range.getBoundingClientRect();
+ return {
+ height: rect.bottom - rect.top,
+ left: rect.left,
+ top: rect.top
+ };
+ };
+
+ EditableCaret.prototype.getOffset = function(pos) {
+ var clonedRange, offset, range, rect, shadowCaret;
+ if (oWindow.getSelection && (range = this.range())) {
+ if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) {
+ clonedRange = range.cloneRange();
+ clonedRange.setStart(range.endContainer, range.endOffset - 1);
+ clonedRange.setEnd(range.endContainer, range.endOffset);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left + rect.width,
+ top: rect.top
+ };
+ clonedRange.detach();
+ }
+ if (!offset || (offset != null ? offset.height : void 0) === 0) {
+ clonedRange = range.cloneRange();
+ shadowCaret = $(oDocument.createTextNode("|"));
+ clonedRange.insertNode(shadowCaret[0]);
+ clonedRange.selectNode(shadowCaret[0]);
+ rect = clonedRange.getBoundingClientRect();
+ offset = {
+ height: rect.height,
+ left: rect.left,
+ top: rect.top
+ };
+ shadowCaret.remove();
+ clonedRange.detach();
+ }
+ } else if (oDocument.selection) {
+ offset = this.getOldIEOffset();
+ }
+ if (offset) {
+ offset.top += $(oWindow).scrollTop();
+ offset.left += $(oWindow).scrollLeft();
+ }
+ return offset;
+ };
+
+ EditableCaret.prototype.range = function() {
+ var sel;
+ if (!oWindow.getSelection) {
+ return;
+ }
+ sel = oWindow.getSelection();
+ if (sel.rangeCount > 0) {
+ return sel.getRangeAt(0);
+ } else {
+ return null;
+ }
+ };
+
+ return EditableCaret;
+
+})();
+
+InputCaret = (function() {
+ function InputCaret($inputor) {
+ this.$inputor = $inputor;
+ this.domInputor = this.$inputor[0];
+ }
+
+ InputCaret.prototype.getIEPos = function() {
+ var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
+ inputor = this.domInputor;
+ range = oDocument.selection.createRange();
+ pos = 0;
+ if (range && range.parentElement() === inputor) {
+ normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+ len = normalizedValue.length;
+ textInputRange = inputor.createTextRange();
+ textInputRange.moveToBookmark(range.getBookmark());
+ endRange = inputor.createTextRange();
+ endRange.collapse(false);
+ if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+ pos = len;
+ } else {
+ pos = -textInputRange.moveStart("character", -len);
+ }
+ }
+ return pos;
+ };
+
+ InputCaret.prototype.getPos = function() {
+ if (oDocument.selection) {
+ return this.getIEPos();
+ } else {
+ return this.domInputor.selectionStart;
+ }
+ };
+
+ InputCaret.prototype.setPos = function(pos) {
+ var inputor, range;
+ inputor = this.domInputor;
+ if (oDocument.selection) {
+ range = inputor.createTextRange();
+ range.move("character", pos);
+ range.select();
+ } else if (inputor.setSelectionRange) {
+ inputor.setSelectionRange(pos, pos);
+ }
+ return inputor;
+ };
+
+ InputCaret.prototype.getIEOffset = function(pos) {
+ var h, textRange, x, y;
+ textRange = this.domInputor.createTextRange();
+ pos || (pos = this.getPos());
+ textRange.move('character', pos);
+ x = textRange.boundingLeft;
+ y = textRange.boundingTop;
+ h = textRange.boundingHeight;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ InputCaret.prototype.getOffset = function(pos) {
+ var $inputor, offset, position;
+ $inputor = this.$inputor;
+ if (oDocument.selection) {
+ offset = this.getIEOffset(pos);
+ offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
+ offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
+ return offset;
+ } else {
+ offset = $inputor.offset();
+ position = this.getPosition(pos);
+ return offset = {
+ left: offset.left + position.left - $inputor.scrollLeft(),
+ top: offset.top + position.top - $inputor.scrollTop(),
+ height: position.height
+ };
+ }
+ };
+
+ InputCaret.prototype.getPosition = function(pos) {
+ var $inputor, at_rect, end_range, format, html, mirror, start_range;
+ $inputor = this.$inputor;
+ format = function(value) {
+ value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>");
+ if (/firefox/i.test(navigator.userAgent)) {
+ value = value.replace(/\s/g, '&nbsp;');
+ }
+ return value;
+ };
+ if (pos === void 0) {
+ pos = this.getPos();
+ }
+ start_range = $inputor.val().slice(0, pos);
+ end_range = $inputor.val().slice(pos);
+ html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
+ html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
+ html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
+ mirror = new Mirror($inputor);
+ return at_rect = mirror.create(html).rect();
+ };
+
+ InputCaret.prototype.getIEPosition = function(pos) {
+ var h, inputorOffset, offset, x, y;
+ offset = this.getIEOffset(pos);
+ inputorOffset = this.$inputor.offset();
+ x = offset.left - inputorOffset.left;
+ y = offset.top - inputorOffset.top;
+ h = offset.height;
+ return {
+ left: x,
+ top: y,
+ height: h
+ };
+ };
+
+ return InputCaret;
+
+})();
+
+Mirror = (function() {
+ Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
+
+ function Mirror($inputor) {
+ this.$inputor = $inputor;
+ }
+
+ Mirror.prototype.mirrorCss = function() {
+ var css,
+ _this = this;
+ css = {
+ position: 'absolute',
+ left: -9999,
+ top: 0,
+ zIndex: -20000
+ };
+ if (this.$inputor.prop('tagName') === 'TEXTAREA') {
+ this.css_attr.push('width');
+ }
+ $.each(this.css_attr, function(i, p) {
+ return css[p] = _this.$inputor.css(p);
+ });
+ return css;
+ };
+
+ Mirror.prototype.create = function(html) {
+ this.$mirror = $('<div></div>');
+ this.$mirror.css(this.mirrorCss());
+ this.$mirror.html(html);
+ this.$inputor.after(this.$mirror);
+ return this;
+ };
+
+ Mirror.prototype.rect = function() {
+ var $flag, pos, rect;
+ $flag = this.$mirror.find("#caret");
+ pos = $flag.position();
+ rect = {
+ left: pos.left,
+ top: pos.top,
+ height: $flag.height()
+ };
+ this.$mirror.remove();
+ return rect;
+ };
+
+ return Mirror;
+
+})();
+
+Utils = {
+ contentEditable: function($inputor) {
+ return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
+ }
+};
+
+methods = {
+ pos: function(pos) {
+ if (pos || pos === 0) {
+ return this.setPos(pos);
+ } else {
+ return this.getPos();
+ }
+ },
+ position: function(pos) {
+ if (oDocument.selection) {
+ return this.getIEPosition(pos);
+ } else {
+ return this.getPosition(pos);
+ }
+ },
+ offset: function(pos) {
+ var offset;
+ offset = this.getOffset(pos);
+ return offset;
+ }
+};
+
+oDocument = null;
+
+oWindow = null;
+
+oFrame = null;
+
+setContextBy = function(settings) {
+ var iframe;
+ if (iframe = settings != null ? settings.iframe : void 0) {
+ oFrame = iframe;
+ oWindow = iframe.contentWindow;
+ return oDocument = iframe.contentDocument || oWindow.document;
+ } else {
+ oFrame = void 0;
+ oWindow = window;
+ return oDocument = document;
+ }
+};
+
+discoveryIframeOf = function($dom) {
+ var error;
+ oDocument = $dom[0].ownerDocument;
+ oWindow = oDocument.defaultView || oDocument.parentWindow;
+ try {
+ return oFrame = oWindow.frameElement;
+ } catch (_error) {
+ error = _error;
+ }
+};
+
+$.fn.caret = function(method, value, settings) {
+ var caret;
+ if (methods[method]) {
+ if ($.isPlainObject(value)) {
+ setContextBy(value);
+ value = void 0;
+ } else {
+ setContextBy(settings);
+ }
+ caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
+ return methods[method].apply(caret, [value]);
+ } else {
+ return $.error("Method " + method + " does not exist on jQuery.caret");
+ }
+};
+
+$.fn.caret.EditableCaret = EditableCaret;
+
+$.fn.caret.InputCaret = InputCaret;
+
+$.fn.caret.Utils = Utils;
+
+$.fn.caret.apis = methods;
+
+
+}));
diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js
deleted file mode 100644
index fd6e95e75d5..00000000000
--- a/vendor/assets/javascripts/jquery.turbolinks.js
+++ /dev/null
@@ -1,49 +0,0 @@
-// Generated by CoffeeScript 1.7.1
-
-/*
-jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks
-jQuery plugin for drop-in fix binded events problem caused by Turbolinks
-
-The MIT License
-Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz
- */
-
-(function() {
- var $, $document;
-
- $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0);
-
- $document = $(document);
-
- $.turbo = {
- version: '2.1.0',
- isReady: false,
- use: function(load, fetch) {
- return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch);
- },
- addCallback: function(callback) {
- if ($.turbo.isReady) {
- callback($);
- }
- return $document.on('turbo:ready', function() {
- return callback($);
- });
- },
- onLoad: function() {
- $.turbo.isReady = true;
- return $document.trigger('turbo:ready');
- },
- onFetch: function() {
- return $.turbo.isReady = false;
- },
- register: function() {
- $(this.onLoad);
- return $.fn.ready = this.addCallback;
- }
- };
-
- $.turbo.register();
-
- $.turbo.use('page:load', 'page:fetch');
-
-}).call(this);
diff --git a/vendor/assets/javascripts/timeago.js b/vendor/assets/javascripts/timeago.js
deleted file mode 100644
index 0eb6f7967a5..00000000000
--- a/vendor/assets/javascripts/timeago.js
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * 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;
-});
diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js
index e666b136051..a33e5e0ade9 100644
--- a/vendor/assets/javascripts/u2f.js
+++ b/vendor/assets/javascripts/u2f.js
@@ -745,4 +745,6 @@ u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
};
port.postMessage(req);
});
-}; \ No newline at end of file
+};
+
+window.u2f || (window.u2f = u2f);
diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js
deleted file mode 100644
index d7981dbec7e..00000000000
--- a/vendor/assets/javascripts/vue-resource.full.js
+++ /dev/null
@@ -1,1318 +0,0 @@
-/*!
- * vue-resource v0.9.3
- * https://github.com/vuejs/vue-resource
- * 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.VueResource = factory());
-}(this, function () { 'use strict';
-
- /**
- * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis)
- */
-
- var RESOLVED = 0;
- var REJECTED = 1;
- var PENDING = 2;
-
- function Promise$2(executor) {
-
- this.state = PENDING;
- this.value = undefined;
- this.deferred = [];
-
- var promise = this;
-
- try {
- executor(function (x) {
- promise.resolve(x);
- }, function (r) {
- promise.reject(r);
- });
- } catch (e) {
- promise.reject(e);
- }
- }
-
- Promise$2.reject = function (r) {
- return new Promise$2(function (resolve, reject) {
- reject(r);
- });
- };
-
- Promise$2.resolve = function (x) {
- return new Promise$2(function (resolve, reject) {
- resolve(x);
- });
- };
-
- Promise$2.all = function all(iterable) {
- return new Promise$2(function (resolve, reject) {
- var count = 0,
- result = [];
-
- if (iterable.length === 0) {
- resolve(result);
- }
-
- function resolver(i) {
- return function (x) {
- result[i] = x;
- count += 1;
-
- if (count === iterable.length) {
- resolve(result);
- }
- };
- }
-
- for (var i = 0; i < iterable.length; i += 1) {
- Promise$2.resolve(iterable[i]).then(resolver(i), reject);
- }
- });
- };
-
- Promise$2.race = function race(iterable) {
- return new Promise$2(function (resolve, reject) {
- for (var i = 0; i < iterable.length; i += 1) {
- Promise$2.resolve(iterable[i]).then(resolve, reject);
- }
- });
- };
-
- var p$1 = Promise$2.prototype;
-
- p$1.resolve = function resolve(x) {
- var promise = this;
-
- if (promise.state === PENDING) {
- if (x === promise) {
- throw new TypeError('Promise settled with itself.');
- }
-
- var called = false;
-
- try {
- var then = x && x['then'];
-
- if (x !== null && typeof x === 'object' && typeof then === 'function') {
- then.call(x, function (x) {
- if (!called) {
- promise.resolve(x);
- }
- called = true;
- }, function (r) {
- if (!called) {
- promise.reject(r);
- }
- called = true;
- });
- return;
- }
- } catch (e) {
- if (!called) {
- promise.reject(e);
- }
- return;
- }
-
- promise.state = RESOLVED;
- promise.value = x;
- promise.notify();
- }
- };
-
- p$1.reject = function reject(reason) {
- var promise = this;
-
- if (promise.state === PENDING) {
- if (reason === promise) {
- throw new TypeError('Promise settled with itself.');
- }
-
- promise.state = REJECTED;
- promise.value = reason;
- promise.notify();
- }
- };
-
- p$1.notify = function notify() {
- var promise = this;
-
- nextTick(function () {
- if (promise.state !== PENDING) {
- while (promise.deferred.length) {
- var deferred = promise.deferred.shift(),
- onResolved = deferred[0],
- onRejected = deferred[1],
- resolve = deferred[2],
- reject = deferred[3];
-
- try {
- if (promise.state === RESOLVED) {
- if (typeof onResolved === 'function') {
- resolve(onResolved.call(undefined, promise.value));
- } else {
- resolve(promise.value);
- }
- } else if (promise.state === REJECTED) {
- if (typeof onRejected === 'function') {
- resolve(onRejected.call(undefined, promise.value));
- } else {
- reject(promise.value);
- }
- }
- } catch (e) {
- reject(e);
- }
- }
- }
- });
- };
-
- p$1.then = function then(onResolved, onRejected) {
- var promise = this;
-
- return new Promise$2(function (resolve, reject) {
- promise.deferred.push([onResolved, onRejected, resolve, reject]);
- promise.notify();
- });
- };
-
- p$1.catch = function (onRejected) {
- return this.then(undefined, onRejected);
- };
-
- var PromiseObj = window.Promise || Promise$2;
-
- function Promise$1(executor, context) {
-
- if (executor instanceof PromiseObj) {
- this.promise = executor;
- } else {
- this.promise = new PromiseObj(executor.bind(context));
- }
-
- this.context = context;
- }
-
- Promise$1.all = function (iterable, context) {
- return new Promise$1(PromiseObj.all(iterable), context);
- };
-
- Promise$1.resolve = function (value, context) {
- return new Promise$1(PromiseObj.resolve(value), context);
- };
-
- Promise$1.reject = function (reason, context) {
- return new Promise$1(PromiseObj.reject(reason), context);
- };
-
- Promise$1.race = function (iterable, context) {
- return new Promise$1(PromiseObj.race(iterable), context);
- };
-
- var p = Promise$1.prototype;
-
- p.bind = function (context) {
- this.context = context;
- return this;
- };
-
- p.then = function (fulfilled, rejected) {
-
- if (fulfilled && fulfilled.bind && this.context) {
- fulfilled = fulfilled.bind(this.context);
- }
-
- if (rejected && rejected.bind && this.context) {
- rejected = rejected.bind(this.context);
- }
-
- return new Promise$1(this.promise.then(fulfilled, rejected), this.context);
- };
-
- p.catch = function (rejected) {
-
- if (rejected && rejected.bind && this.context) {
- rejected = rejected.bind(this.context);
- }
-
- return new Promise$1(this.promise.catch(rejected), this.context);
- };
-
- p.finally = function (callback) {
-
- return this.then(function (value) {
- callback.call(this);
- return value;
- }, function (reason) {
- callback.call(this);
- return PromiseObj.reject(reason);
- });
- };
-
- var debug = false;
- var util = {};
- var array = [];
- function Util (Vue) {
- util = Vue.util;
- debug = Vue.config.debug || !Vue.config.silent;
- }
-
- function warn(msg) {
- if (typeof console !== 'undefined' && debug) {
- console.warn('[VueResource warn]: ' + msg);
- }
- }
-
- function error(msg) {
- if (typeof console !== 'undefined') {
- console.error(msg);
- }
- }
-
- function nextTick(cb, ctx) {
- return util.nextTick(cb, ctx);
- }
-
- function trim(str) {
- return str.replace(/^\s*|\s*$/g, '');
- }
-
- var isArray = Array.isArray;
-
- function isString(val) {
- return typeof val === 'string';
- }
-
- function isBoolean(val) {
- return val === true || val === false;
- }
-
- function isFunction(val) {
- return typeof val === 'function';
- }
-
- function isObject(obj) {
- return obj !== null && typeof obj === 'object';
- }
-
- function isPlainObject(obj) {
- return isObject(obj) && Object.getPrototypeOf(obj) == Object.prototype;
- }
-
- function isFormData(obj) {
- return typeof FormData !== 'undefined' && obj instanceof FormData;
- }
-
- function when(value, fulfilled, rejected) {
-
- var promise = Promise$1.resolve(value);
-
- if (arguments.length < 2) {
- return promise;
- }
-
- return promise.then(fulfilled, rejected);
- }
-
- function options(fn, obj, opts) {
-
- opts = opts || {};
-
- if (isFunction(opts)) {
- opts = opts.call(obj);
- }
-
- return merge(fn.bind({ $vm: obj, $options: opts }), fn, { $options: opts });
- }
-
- function each(obj, iterator) {
-
- var i, key;
-
- if (typeof obj.length == 'number') {
- for (i = 0; i < obj.length; i++) {
- iterator.call(obj[i], obj[i], i);
- }
- } else if (isObject(obj)) {
- for (key in obj) {
- if (obj.hasOwnProperty(key)) {
- iterator.call(obj[key], obj[key], key);
- }
- }
- }
-
- return obj;
- }
-
- var assign = Object.assign || _assign;
-
- function merge(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
- _merge(target, source, true);
- });
-
- return target;
- }
-
- function defaults(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
-
- for (var key in source) {
- if (target[key] === undefined) {
- target[key] = source[key];
- }
- }
- });
-
- return target;
- }
-
- function _assign(target) {
-
- var args = array.slice.call(arguments, 1);
-
- args.forEach(function (source) {
- _merge(target, source);
- });
-
- return target;
- }
-
- function _merge(target, source, deep) {
- for (var key in source) {
- if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
- if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
- target[key] = {};
- }
- if (isArray(source[key]) && !isArray(target[key])) {
- target[key] = [];
- }
- _merge(target[key], source[key], deep);
- } else if (source[key] !== undefined) {
- target[key] = source[key];
- }
- }
- }
-
- function root (options, next) {
-
- var url = next(options);
-
- if (isString(options.root) && !url.match(/^(https?:)?\//)) {
- url = options.root + '/' + url;
- }
-
- return url;
- }
-
- function query (options, next) {
-
- var urlParams = Object.keys(Url.options.params),
- query = {},
- url = next(options);
-
- each(options.params, function (value, key) {
- if (urlParams.indexOf(key) === -1) {
- query[key] = value;
- }
- });
-
- query = Url.params(query);
-
- if (query) {
- url += (url.indexOf('?') == -1 ? '?' : '&') + query;
- }
-
- return url;
- }
-
- /**
- * URL Template v2.0.6 (https://github.com/bramstein/url-template)
- */
-
- function expand(url, params, variables) {
-
- var tmpl = parse(url),
- expanded = tmpl.expand(params);
-
- if (variables) {
- variables.push.apply(variables, tmpl.vars);
- }
-
- return expanded;
- }
-
- function parse(template) {
-
- var operators = ['+', '#', '.', '/', ';', '?', '&'],
- variables = [];
-
- return {
- vars: variables,
- expand: function (context) {
- return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
- if (expression) {
-
- var operator = null,
- values = [];
-
- if (operators.indexOf(expression.charAt(0)) !== -1) {
- operator = expression.charAt(0);
- expression = expression.substr(1);
- }
-
- expression.split(/,/g).forEach(function (variable) {
- var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
- values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
- variables.push(tmp[1]);
- });
-
- if (operator && operator !== '+') {
-
- var separator = ',';
-
- if (operator === '?') {
- separator = '&';
- } else if (operator !== '#') {
- separator = operator;
- }
-
- return (values.length !== 0 ? operator : '') + values.join(separator);
- } else {
- return values.join(',');
- }
- } else {
- return encodeReserved(literal);
- }
- });
- }
- };
- }
-
- function getValues(context, operator, key, modifier) {
-
- var value = context[key],
- result = [];
-
- if (isDefined(value) && value !== '') {
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
- value = value.toString();
-
- if (modifier && modifier !== '*') {
- value = value.substring(0, parseInt(modifier, 10));
- }
-
- result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
- } else {
- if (modifier === '*') {
- if (Array.isArray(value)) {
- value.filter(isDefined).forEach(function (value) {
- result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
- });
- } else {
- Object.keys(value).forEach(function (k) {
- if (isDefined(value[k])) {
- result.push(encodeValue(operator, value[k], k));
- }
- });
- }
- } else {
- var tmp = [];
-
- if (Array.isArray(value)) {
- value.filter(isDefined).forEach(function (value) {
- tmp.push(encodeValue(operator, value));
- });
- } else {
- Object.keys(value).forEach(function (k) {
- if (isDefined(value[k])) {
- tmp.push(encodeURIComponent(k));
- tmp.push(encodeValue(operator, value[k].toString()));
- }
- });
- }
-
- if (isKeyOperator(operator)) {
- result.push(encodeURIComponent(key) + '=' + tmp.join(','));
- } else if (tmp.length !== 0) {
- result.push(tmp.join(','));
- }
- }
- }
- } else {
- if (operator === ';') {
- result.push(encodeURIComponent(key));
- } else if (value === '' && (operator === '&' || operator === '?')) {
- result.push(encodeURIComponent(key) + '=');
- } else if (value === '') {
- result.push('');
- }
- }
-
- return result;
- }
-
- function isDefined(value) {
- return value !== undefined && value !== null;
- }
-
- function isKeyOperator(operator) {
- return operator === ';' || operator === '&' || operator === '?';
- }
-
- function encodeValue(operator, value, key) {
-
- value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeURIComponent(value);
-
- if (key) {
- return encodeURIComponent(key) + '=' + value;
- } else {
- return value;
- }
- }
-
- function encodeReserved(str) {
- return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) {
- if (!/%[0-9A-Fa-f]/.test(part)) {
- part = encodeURI(part);
- }
- return part;
- }).join('');
- }
-
- function template (options) {
-
- var variables = [],
- url = expand(options.url, options.params, variables);
-
- variables.forEach(function (key) {
- delete options.params[key];
- });
-
- return url;
- }
-
- /**
- * Service for URL templating.
- */
-
- var ie = document.documentMode;
- var el = document.createElement('a');
-
- function Url(url, params) {
-
- var self = this || {},
- options = url,
- transform;
-
- if (isString(url)) {
- options = { url: url, params: params };
- }
-
- options = merge({}, Url.options, self.$options, options);
-
- Url.transforms.forEach(function (handler) {
- transform = factory(handler, transform, self.$vm);
- });
-
- return transform(options);
- }
-
- /**
- * Url options.
- */
-
- Url.options = {
- url: '',
- root: null,
- params: {}
- };
-
- /**
- * Url transforms.
- */
-
- Url.transforms = [template, query, root];
-
- /**
- * Encodes a Url parameter string.
- *
- * @param {Object} obj
- */
-
- Url.params = function (obj) {
-
- var params = [],
- escape = encodeURIComponent;
-
- params.add = function (key, value) {
-
- if (isFunction(value)) {
- value = value();
- }
-
- if (value === null) {
- value = '';
- }
-
- this.push(escape(key) + '=' + escape(value));
- };
-
- serialize(params, obj);
-
- return params.join('&').replace(/%20/g, '+');
- };
-
- /**
- * Parse a URL and return its components.
- *
- * @param {String} url
- */
-
- Url.parse = function (url) {
-
- if (ie) {
- el.href = url;
- url = el.href;
- }
-
- el.href = url;
-
- return {
- href: el.href,
- protocol: el.protocol ? el.protocol.replace(/:$/, '') : '',
- port: el.port,
- host: el.host,
- hostname: el.hostname,
- pathname: el.pathname.charAt(0) === '/' ? el.pathname : '/' + el.pathname,
- search: el.search ? el.search.replace(/^\?/, '') : '',
- hash: el.hash ? el.hash.replace(/^#/, '') : ''
- };
- };
-
- function factory(handler, next, vm) {
- return function (options) {
- return handler.call(vm, options, next);
- };
- }
-
- function serialize(params, obj, scope) {
-
- var array = isArray(obj),
- plain = isPlainObject(obj),
- hash;
-
- each(obj, function (value, key) {
-
- hash = isObject(value) || isArray(value);
-
- if (scope) {
- key = scope + '[' + (plain || hash ? key : '') + ']';
- }
-
- if (!scope && array) {
- params.add(value.name, value.value);
- } else if (hash) {
- serialize(params, value, key);
- } else {
- params.add(key, value);
- }
- });
- }
-
- function xdrClient (request) {
- return new Promise$1(function (resolve) {
-
- var xdr = new XDomainRequest(),
- handler = function (event) {
-
- var response = request.respondWith(xdr.responseText, {
- status: xdr.status,
- statusText: xdr.statusText
- });
-
- resolve(response);
- };
-
- request.abort = function () {
- return xdr.abort();
- };
-
- xdr.open(request.method, request.getUrl(), true);
- xdr.timeout = 0;
- xdr.onload = handler;
- xdr.onerror = handler;
- xdr.ontimeout = function () {};
- xdr.onprogress = function () {};
- xdr.send(request.getBody());
- });
- }
-
- var ORIGIN_URL = Url.parse(location.href);
- var SUPPORTS_CORS = 'withCredentials' in new XMLHttpRequest();
-
- function cors (request, next) {
-
- if (!isBoolean(request.crossOrigin) && crossOrigin(request)) {
- request.crossOrigin = true;
- }
-
- if (request.crossOrigin) {
-
- if (!SUPPORTS_CORS) {
- request.client = xdrClient;
- }
-
- delete request.emulateHTTP;
- }
-
- next();
- }
-
- function crossOrigin(request) {
-
- var requestUrl = Url.parse(Url(request));
-
- return requestUrl.protocol !== ORIGIN_URL.protocol || requestUrl.host !== ORIGIN_URL.host;
- }
-
- function body (request, next) {
-
- if (request.emulateJSON && isPlainObject(request.body)) {
- request.body = Url.params(request.body);
- request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
- if (isFormData(request.body)) {
- delete request.headers['Content-Type'];
- }
-
- if (isPlainObject(request.body)) {
- request.body = JSON.stringify(request.body);
- }
-
- next(function (response) {
-
- var contentType = response.headers['Content-Type'];
-
- if (isString(contentType) && contentType.indexOf('application/json') === 0) {
-
- try {
- response.data = response.json();
- } catch (e) {
- response.data = null;
- }
- } else {
- response.data = response.text();
- }
- });
- }
-
- function jsonpClient (request) {
- return new Promise$1(function (resolve) {
-
- var name = request.jsonp || 'callback',
- callback = '_jsonp' + Math.random().toString(36).substr(2),
- body = null,
- handler,
- script;
-
- handler = function (event) {
-
- var status = 0;
-
- if (event.type === 'load' && body !== null) {
- status = 200;
- } else if (event.type === 'error') {
- status = 404;
- }
-
- resolve(request.respondWith(body, { status: status }));
-
- delete window[callback];
- document.body.removeChild(script);
- };
-
- request.params[name] = callback;
-
- window[callback] = function (result) {
- body = JSON.stringify(result);
- };
-
- script = document.createElement('script');
- script.src = request.getUrl();
- script.type = 'text/javascript';
- script.async = true;
- script.onload = handler;
- script.onerror = handler;
-
- document.body.appendChild(script);
- });
- }
-
- function jsonp (request, next) {
-
- if (request.method == 'JSONP') {
- request.client = jsonpClient;
- }
-
- next(function (response) {
-
- if (request.method == 'JSONP') {
- response.data = response.json();
- }
- });
- }
-
- function before (request, next) {
-
- if (isFunction(request.before)) {
- request.before.call(this, request);
- }
-
- next();
- }
-
- /**
- * HTTP method override Interceptor.
- */
-
- function method (request, next) {
-
- if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) {
- request.headers['X-HTTP-Method-Override'] = request.method;
- request.method = 'POST';
- }
-
- next();
- }
-
- function header (request, next) {
-
- request.method = request.method.toUpperCase();
- request.headers = assign({}, Http.headers.common, !request.crossOrigin ? Http.headers.custom : {}, Http.headers[request.method.toLowerCase()], request.headers);
-
- next();
- }
-
- /**
- * Timeout Interceptor.
- */
-
- function timeout (request, next) {
-
- var timeout;
-
- if (request.timeout) {
- timeout = setTimeout(function () {
- request.abort();
- }, request.timeout);
- }
-
- next(function (response) {
-
- clearTimeout(timeout);
- });
- }
-
- function xhrClient (request) {
- return new Promise$1(function (resolve) {
-
- var xhr = new XMLHttpRequest(),
- handler = function (event) {
-
- var response = request.respondWith('response' in xhr ? xhr.response : xhr.responseText, {
- status: xhr.status === 1223 ? 204 : xhr.status, // IE9 status bug
- statusText: xhr.status === 1223 ? 'No Content' : trim(xhr.statusText),
- headers: parseHeaders(xhr.getAllResponseHeaders())
- });
-
- resolve(response);
- };
-
- request.abort = function () {
- return xhr.abort();
- };
-
- xhr.open(request.method, request.getUrl(), true);
- xhr.timeout = 0;
- xhr.onload = handler;
- xhr.onerror = handler;
-
- if (request.progress) {
- if (request.method === 'GET') {
- xhr.addEventListener('progress', request.progress);
- } else if (/^(POST|PUT)$/i.test(request.method)) {
- xhr.upload.addEventListener('progress', request.progress);
- }
- }
-
- if (request.credentials === true) {
- xhr.withCredentials = true;
- }
-
- each(request.headers || {}, function (value, header) {
- xhr.setRequestHeader(header, value);
- });
-
- xhr.send(request.getBody());
- });
- }
-
- function parseHeaders(str) {
-
- var headers = {},
- value,
- name,
- i;
-
- each(trim(str).split('\n'), function (row) {
-
- i = row.indexOf(':');
- name = trim(row.slice(0, i));
- value = trim(row.slice(i + 1));
-
- if (headers[name]) {
-
- if (isArray(headers[name])) {
- headers[name].push(value);
- } else {
- headers[name] = [headers[name], value];
- }
- } else {
-
- headers[name] = value;
- }
- });
-
- return headers;
- }
-
- function Client (context) {
-
- var reqHandlers = [sendRequest],
- resHandlers = [],
- handler;
-
- if (!isObject(context)) {
- context = null;
- }
-
- function Client(request) {
- return new Promise$1(function (resolve) {
-
- function exec() {
-
- handler = reqHandlers.pop();
-
- if (isFunction(handler)) {
- handler.call(context, request, next);
- } else {
- warn('Invalid interceptor of type ' + typeof handler + ', must be a function');
- next();
- }
- }
-
- function next(response) {
-
- if (isFunction(response)) {
-
- resHandlers.unshift(response);
- } else if (isObject(response)) {
-
- resHandlers.forEach(function (handler) {
- response = when(response, function (response) {
- return handler.call(context, response) || response;
- });
- });
-
- when(response, resolve);
-
- return;
- }
-
- exec();
- }
-
- exec();
- }, context);
- }
-
- Client.use = function (handler) {
- reqHandlers.push(handler);
- };
-
- return Client;
- }
-
- function sendRequest(request, resolve) {
-
- var client = request.client || xhrClient;
-
- resolve(client(request));
- }
-
- var classCallCheck = function (instance, Constructor) {
- if (!(instance instanceof Constructor)) {
- throw new TypeError("Cannot call a class as a function");
- }
- };
-
- /**
- * HTTP Response.
- */
-
- var Response = function () {
- function Response(body, _ref) {
- var url = _ref.url;
- var headers = _ref.headers;
- var status = _ref.status;
- var statusText = _ref.statusText;
- classCallCheck(this, Response);
-
-
- this.url = url;
- this.body = body;
- this.headers = headers || {};
- this.status = status || 0;
- this.statusText = statusText || '';
- this.ok = status >= 200 && status < 300;
- }
-
- Response.prototype.text = function text() {
- return this.body;
- };
-
- Response.prototype.blob = function blob() {
- return new Blob([this.body]);
- };
-
- Response.prototype.json = function json() {
- return JSON.parse(this.body);
- };
-
- return Response;
- }();
-
- var Request = function () {
- function Request(options) {
- classCallCheck(this, Request);
-
-
- this.method = 'GET';
- this.body = null;
- this.params = {};
- this.headers = {};
-
- assign(this, options);
- }
-
- Request.prototype.getUrl = function getUrl() {
- return Url(this);
- };
-
- Request.prototype.getBody = function getBody() {
- return this.body;
- };
-
- Request.prototype.respondWith = function respondWith(body, options) {
- return new Response(body, assign(options || {}, { url: this.getUrl() }));
- };
-
- return Request;
- }();
-
- /**
- * Service for sending network requests.
- */
-
- var CUSTOM_HEADERS = { 'X-Requested-With': 'XMLHttpRequest' };
- var COMMON_HEADERS = { 'Accept': 'application/json, text/plain, */*' };
- var JSON_CONTENT_TYPE = { 'Content-Type': 'application/json;charset=utf-8' };
-
- function Http(options) {
-
- var self = this || {},
- client = Client(self.$vm);
-
- defaults(options || {}, self.$options, Http.options);
-
- Http.interceptors.forEach(function (handler) {
- client.use(handler);
- });
-
- return client(new Request(options)).then(function (response) {
-
- return response.ok ? response : Promise$1.reject(response);
- }, function (response) {
-
- if (response instanceof Error) {
- error(response);
- }
-
- return Promise$1.reject(response);
- });
- }
-
- Http.options = {};
-
- Http.headers = {
- put: JSON_CONTENT_TYPE,
- post: JSON_CONTENT_TYPE,
- patch: JSON_CONTENT_TYPE,
- delete: JSON_CONTENT_TYPE,
- custom: CUSTOM_HEADERS,
- common: COMMON_HEADERS
- };
-
- Http.interceptors = [before, timeout, method, body, jsonp, header, cors];
-
- ['get', 'delete', 'head', 'jsonp'].forEach(function (method) {
-
- Http[method] = function (url, options) {
- return this(assign(options || {}, { url: url, method: method }));
- };
- });
-
- ['post', 'put', 'patch'].forEach(function (method) {
-
- Http[method] = function (url, body, options) {
- return this(assign(options || {}, { url: url, method: method, body: body }));
- };
- });
-
- function Resource(url, params, actions, options) {
-
- var self = this || {},
- resource = {};
-
- actions = assign({}, Resource.actions, actions);
-
- each(actions, function (action, name) {
-
- action = merge({ url: url, params: params || {} }, options, action);
-
- resource[name] = function () {
- return (self.$http || Http)(opts(action, arguments));
- };
- });
-
- return resource;
- }
-
- function opts(action, args) {
-
- var options = assign({}, action),
- params = {},
- body;
-
- switch (args.length) {
-
- case 2:
-
- params = args[0];
- body = args[1];
-
- break;
-
- case 1:
-
- if (/^(POST|PUT|PATCH)$/i.test(options.method)) {
- body = args[0];
- } else {
- params = args[0];
- }
-
- break;
-
- case 0:
-
- break;
-
- default:
-
- throw 'Expected up to 4 arguments [params, body], got ' + args.length + ' arguments';
- }
-
- options.body = body;
- options.params = assign({}, options.params, params);
-
- return options;
- }
-
- Resource.actions = {
-
- get: { method: 'GET' },
- save: { method: 'POST' },
- query: { method: 'GET' },
- update: { method: 'PUT' },
- remove: { method: 'DELETE' },
- delete: { method: 'DELETE' }
-
- };
-
- function plugin(Vue) {
-
- if (plugin.installed) {
- return;
- }
-
- Util(Vue);
-
- Vue.url = Url;
- Vue.http = Http;
- Vue.resource = Resource;
- Vue.Promise = Promise$1;
-
- Object.defineProperties(Vue.prototype, {
-
- $url: {
- get: function () {
- return options(Vue.url, this, this.$options.url);
- }
- },
-
- $http: {
- get: function () {
- return options(Vue.http, this, this.$options.http);
- }
- },
-
- $resource: {
- get: function () {
- return Vue.resource.bind(this);
- }
- },
-
- $promise: {
- get: function () {
- var _this = this;
-
- return function (executor) {
- return new Vue.Promise(executor, _this);
- };
- }
- }
-
- });
- }
-
- if (typeof window !== 'undefined' && window.Vue) {
- window.Vue.use(plugin);
- }
-
- return plugin;
-
-})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue-resource.js.erb b/vendor/assets/javascripts/vue-resource.js.erb
deleted file mode 100644
index 8001775ce98..00000000000
--- a/vendor/assets/javascripts/vue-resource.js.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% type = Rails.env.development? ? 'full' : 'min' %>
-<%= File.read(Rails.root.join("vendor/assets/javascripts/vue-resource.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue-resource.min.js b/vendor/assets/javascripts/vue-resource.min.js
deleted file mode 100644
index 6bff73a2a67..00000000000
--- a/vendor/assets/javascripts/vue-resource.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * vue-resource v0.9.3
- * https://github.com/vuejs/vue-resource
- * Released under the MIT License.
- */
-
-!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=Z,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(e){n.reject(e)}}function n(t,n){t instanceof nt?this.promise=t:this.promise=new nt(t.bind(n)),this.context=n}function e(t){rt=t.util,ot=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ot&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return rt.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return"string"==typeof t}function c(t){return t===!0||t===!1}function a(t){return"function"==typeof t}function f(t){return null!==t&&"object"==typeof t}function h(t){return f(t)&&Object.getPrototypeOf(t)==Object.prototype}function p(t){return"undefined"!=typeof FormData&&t instanceof FormData}function l(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function d(t,n,e){return e=e||{},a(e)&&(e=e.call(n)),v(t.bind({$vm:n,$options:e}),t,{$options:e})}function m(t,n){var e,o;if("number"==typeof t.length)for(e=0;e<t.length;e++)n.call(t[e],t[e],e);else if(f(t))for(o in t)t.hasOwnProperty(o)&&n.call(t[o],t[o],o);return t}function v(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n,!0)}),t}function y(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){for(var e in n)void 0===t[e]&&(t[e]=n[e])}),t}function b(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n)}),t}function g(t,n,e){for(var o in n)e&&(h(n[o])||ut(n[o]))?(h(n[o])&&!h(t[o])&&(t[o]={}),ut(n[o])&&!ut(t[o])&&(t[o]=[]),g(t[o],n[o],e)):void 0!==n[o]&&(t[o]=n[o])}function w(t,n){var e=n(t);return s(t.root)&&!e.match(/^(https?:)?\//)&&(e=t.root+"/"+e),e}function T(t,n){var e=Object.keys(R.options.params),o={},r=n(t);return m(t.params,function(t,n){e.indexOf(n)===-1&&(o[n]=t)}),o=R.params(o),o&&(r+=(r.indexOf("?")==-1?"?":"&")+o),r}function j(t,n,e){var o=E(t),r=o.expand(n);return e&&e.push.apply(e,o.vars),r}function E(t){var n=["+","#",".","/",";","?","&"],e=[];return{vars:e,expand:function(o){return t.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g,function(t,r,i){if(r){var u=null,s=[];if(n.indexOf(r.charAt(0))!==-1&&(u=r.charAt(0),r=r.substr(1)),r.split(/,/g).forEach(function(t){var n=/([^:\*]*)(?::(\d+)|(\*))?/.exec(t);s.push.apply(s,x(o,u,n[1],n[2]||n[3])),e.push(n[1])}),u&&"+"!==u){var c=",";return"?"===u?c="&":"#"!==u&&(c=u),(0!==s.length?u:"")+s.join(c)}return s.join(",")}return $(i)})}}}function x(t,n,e,o){var r=t[e],i=[];if(O(r)&&""!==r)if("string"==typeof r||"number"==typeof r||"boolean"==typeof r)r=r.toString(),o&&"*"!==o&&(r=r.substring(0,parseInt(o,10))),i.push(C(n,r,P(n)?e:null));else if("*"===o)Array.isArray(r)?r.filter(O).forEach(function(t){i.push(C(n,t,P(n)?e:null))}):Object.keys(r).forEach(function(t){O(r[t])&&i.push(C(n,r[t],t))});else{var u=[];Array.isArray(r)?r.filter(O).forEach(function(t){u.push(C(n,t))}):Object.keys(r).forEach(function(t){O(r[t])&&(u.push(encodeURIComponent(t)),u.push(C(n,r[t].toString())))}),P(n)?i.push(encodeURIComponent(e)+"="+u.join(",")):0!==u.length&&i.push(u.join(","))}else";"===n?i.push(encodeURIComponent(e)):""!==r||"&"!==n&&"?"!==n?""===r&&i.push(""):i.push(encodeURIComponent(e)+"=");return i}function O(t){return void 0!==t&&null!==t}function P(t){return";"===t||"&"===t||"?"===t}function C(t,n,e){return n="+"===t||"#"===t?$(n):encodeURIComponent(n),e?encodeURIComponent(e)+"="+n:n}function $(t){return t.split(/(%[0-9A-Fa-f]{2})/g).map(function(t){return/%[0-9A-Fa-f]/.test(t)||(t=encodeURI(t)),t}).join("")}function U(t){var n=[],e=j(t.url,t.params,n);return n.forEach(function(n){delete t.params[n]}),e}function R(t,n){var e,o=this||{},r=t;return s(t)&&(r={url:t,params:n}),r=v({},R.options,o.$options,r),R.transforms.forEach(function(t){e=A(t,e,o.$vm)}),e(r)}function A(t,n,e){return function(o){return t.call(e,o,n)}}function S(t,n,e){var o,r=ut(n),i=h(n);m(n,function(n,u){o=f(n)||ut(n),e&&(u=e+"["+(i||o?u:"")+"]"),!e&&r?t.add(n.name,n.value):o?S(t,n,u):t.add(u,n)})}function k(t){return new n(function(n){var e=new XDomainRequest,o=function(o){var r=t.respondWith(e.responseText,{status:e.status,statusText:e.statusText});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,e.ontimeout=function(){},e.onprogress=function(){},e.send(t.getBody())})}function H(t,n){!c(t.crossOrigin)&&I(t)&&(t.crossOrigin=!0),t.crossOrigin&&(ht||(t.client=k),delete t.emulateHTTP),n()}function I(t){var n=R.parse(R(t));return n.protocol!==ft.protocol||n.host!==ft.host}function L(t,n){t.emulateJSON&&h(t.body)&&(t.body=R.params(t.body),t.headers["Content-Type"]="application/x-www-form-urlencoded"),p(t.body)&&delete t.headers["Content-Type"],h(t.body)&&(t.body=JSON.stringify(t.body)),n(function(t){var n=t.headers["Content-Type"];if(s(n)&&0===n.indexOf("application/json"))try{t.data=t.json()}catch(e){t.data=null}else t.data=t.text()})}function q(t){return new n(function(n){var e,o,r=t.jsonp||"callback",i="_jsonp"+Math.random().toString(36).substr(2),u=null;e=function(e){var r=0;"load"===e.type&&null!==u?r=200:"error"===e.type&&(r=404),n(t.respondWith(u,{status:r})),delete window[i],document.body.removeChild(o)},t.params[r]=i,window[i]=function(t){u=JSON.stringify(t)},o=document.createElement("script"),o.src=t.getUrl(),o.type="text/javascript",o.async=!0,o.onload=e,o.onerror=e,document.body.appendChild(o)})}function N(t,n){"JSONP"==t.method&&(t.client=q),n(function(n){"JSONP"==t.method&&(n.data=n.json())})}function D(t,n){a(t.before)&&t.before.call(this,t),n()}function J(t,n){t.emulateHTTP&&/^(PUT|PATCH|DELETE)$/i.test(t.method)&&(t.headers["X-HTTP-Method-Override"]=t.method,t.method="POST"),n()}function M(t,n){t.method=t.method.toUpperCase(),t.headers=st({},V.headers.common,t.crossOrigin?{}:V.headers.custom,V.headers[t.method.toLowerCase()],t.headers),n()}function X(t,n){var e;t.timeout&&(e=setTimeout(function(){t.abort()},t.timeout)),n(function(t){clearTimeout(e)})}function W(t){return new n(function(n){var e=new XMLHttpRequest,o=function(o){var r=t.respondWith("response"in e?e.response:e.responseText,{status:1223===e.status?204:e.status,statusText:1223===e.status?"No Content":u(e.statusText),headers:B(e.getAllResponseHeaders())});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,t.progress&&("GET"===t.method?e.addEventListener("progress",t.progress):/^(POST|PUT)$/i.test(t.method)&&e.upload.addEventListener("progress",t.progress)),t.credentials===!0&&(e.withCredentials=!0),m(t.headers||{},function(t,n){e.setRequestHeader(n,t)}),e.send(t.getBody())})}function B(t){var n,e,o,r={};return m(u(t).split("\n"),function(t){o=t.indexOf(":"),e=u(t.slice(0,o)),n=u(t.slice(o+1)),r[e]?ut(r[e])?r[e].push(n):r[e]=[r[e],n]:r[e]=n}),r}function F(t){function e(e){return new n(function(n){function s(){r=i.pop(),a(r)?r.call(t,e,c):(o("Invalid interceptor of type "+typeof r+", must be a function"),c())}function c(e){if(a(e))u.unshift(e);else if(f(e))return u.forEach(function(n){e=l(e,function(e){return n.call(t,e)||e})}),void l(e,n);s()}s()},t)}var r,i=[G],u=[];return f(t)||(t=null),e.use=function(t){i.push(t)},e}function G(t,n){var e=t.client||W;n(e(t))}function V(t){var e=this||{},o=F(e.$vm);return y(t||{},e.$options,V.options),V.interceptors.forEach(function(t){o.use(t)}),o(new dt(t)).then(function(t){return t.ok?t:n.reject(t)},function(t){return t instanceof Error&&r(t),n.reject(t)})}function _(t,n,e,o){var r=this||{},i={};return e=st({},_.actions,e),m(e,function(e,u){e=v({url:t,params:n||{}},o,e),i[u]=function(){return(r.$http||V)(z(e,arguments))}}),i}function z(t,n){var e,o=st({},t),r={};switch(n.length){case 2:r=n[0],e=n[1];break;case 1:/^(POST|PUT|PATCH)$/i.test(o.method)?e=n[0]:r=n[0];break;case 0:break;default:throw"Expected up to 4 arguments [params, body], got "+n.length+" arguments"}return o.body=e,o.params=st({},o.params,r),o}function K(t){K.installed||(e(t),t.url=R,t.http=V,t.resource=_,t.Promise=n,Object.defineProperties(t.prototype,{$url:{get:function(){return d(t.url,this,this.$options.url)}},$http:{get:function(){return d(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}},$promise:{get:function(){var n=this;return function(e){return new t.Promise(e,n)}}}}))}var Q=0,Y=1,Z=2;t.reject=function(n){return new t(function(t,e){e(n)})},t.resolve=function(n){return new t(function(t,e){t(n)})},t.all=function(n){return new t(function(e,o){function r(t){return function(o){u[t]=o,i+=1,i===n.length&&e(u)}}var i=0,u=[];0===n.length&&e(u);for(var s=0;s<n.length;s+=1)t.resolve(n[s]).then(r(s),o)})},t.race=function(n){return new t(function(e,o){for(var r=0;r<n.length;r+=1)t.resolve(n[r]).then(e,o)})};var tt=t.prototype;tt.resolve=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");var e=!1;try{var o=t&&t.then;if(null!==t&&"object"==typeof t&&"function"==typeof o)return void o.call(t,function(t){e||n.resolve(t),e=!0},function(t){e||n.reject(t),e=!0})}catch(r){return void(e||n.reject(r))}n.state=Q,n.value=t,n.notify()}},tt.reject=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");n.state=Y,n.value=t,n.notify()}},tt.notify=function(){var t=this;i(function(){if(t.state!==Z)for(;t.deferred.length;){var n=t.deferred.shift(),e=n[0],o=n[1],r=n[2],i=n[3];try{t.state===Q?r("function"==typeof e?e.call(void 0,t.value):t.value):t.state===Y&&("function"==typeof o?r(o.call(void 0,t.value)):i(t.value))}catch(u){i(u)}}})},tt.then=function(n,e){var o=this;return new t(function(t,r){o.deferred.push([n,e,t,r]),o.notify()})},tt["catch"]=function(t){return this.then(void 0,t)};var nt=window.Promise||t;n.all=function(t,e){return new n(nt.all(t),e)},n.resolve=function(t,e){return new n(nt.resolve(t),e)},n.reject=function(t,e){return new n(nt.reject(t),e)},n.race=function(t,e){return new n(nt.race(t),e)};var et=n.prototype;et.bind=function(t){return this.context=t,this},et.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),new n(this.promise.then(t,e),this.context)},et["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),new n(this.promise["catch"](t),this.context)},et["finally"]=function(t){return this.then(function(n){return t.call(this),n},function(n){return t.call(this),nt.reject(n)})};var ot=!1,rt={},it=[],ut=Array.isArray,st=Object.assign||b,ct=document.documentMode,at=document.createElement("a");R.options={url:"",root:null,params:{}},R.transforms=[U,T,w],R.params=function(t){var n=[],e=encodeURIComponent;return n.add=function(t,n){a(n)&&(n=n()),null===n&&(n=""),this.push(e(t)+"="+e(n))},S(n,t),n.join("&").replace(/%20/g,"+")},R.parse=function(t){return ct&&(at.href=t,t=at.href),at.href=t,{href:at.href,protocol:at.protocol?at.protocol.replace(/:$/,""):"",port:at.port,host:at.host,hostname:at.hostname,pathname:"/"===at.pathname.charAt(0)?at.pathname:"/"+at.pathname,search:at.search?at.search.replace(/^\?/,""):"",hash:at.hash?at.hash.replace(/^#/,""):""}};var ft=R.parse(location.href),ht="withCredentials"in new XMLHttpRequest,pt=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},lt=function(){function t(n,e){var o=e.url,r=e.headers,i=e.status,u=e.statusText;pt(this,t),this.url=o,this.body=n,this.headers=r||{},this.status=i||0,this.statusText=u||"",this.ok=i>=200&&i<300}return t.prototype.text=function(){return this.body},t.prototype.blob=function(){return new Blob([this.body])},t.prototype.json=function(){return JSON.parse(this.body)},t}(),dt=function(){function t(n){pt(this,t),this.method="GET",this.body=null,this.params={},this.headers={},st(this,n)}return t.prototype.getUrl=function(){return R(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new lt(t,st(n||{},{url:this.getUrl()}))},t}(),mt={"X-Requested-With":"XMLHttpRequest"},vt={Accept:"application/json, text/plain, */*"},yt={"Content-Type":"application/json;charset=utf-8"};return V.options={},V.headers={put:yt,post:yt,patch:yt,"delete":yt,custom:mt,common:vt},V.interceptors=[D,X,J,L,N,M,H],["get","delete","head","jsonp"].forEach(function(t){V[t]=function(n,e){return this(st(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){V[t]=function(n,e,o){return this(st(o||{},{url:n,method:t,body:e}))}}),_.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},"delete":{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(K),K}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
deleted file mode 100644
index ea15bfac416..00000000000
--- a/vendor/assets/javascripts/vue.full.js
+++ /dev/null
@@ -1,7515 +0,0 @@
-/*!
- * 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';
-
-/* */
-
-/**
- * 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);
-
-/**
- * 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)
- }
- }
-}
-
-/**
- * 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'
-}
-
-/**
- * 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))
- }
-}
-
-/**
- * Camelize a hyphen-delmited string.
- */
-var camelizeRE = /-(\w)/g;
-var camelize = cached(function (str) {
- return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
-});
-
-/**
- * Capitalize a string.
- */
-var capitalize = cached(function (str) {
- return str.charAt(0).toUpperCase() + str.slice(1)
-});
-
-/**
- * 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
-}
-
-/**
- * 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'
-}
-
-/**
- * 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
-}
-
-/**
- * Perform no operation.
- */
-function noop () {}
-
-/**
- * Always return false.
- */
-var no = function () { return false; };
-
-/**
- * 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 looseIndexOf (arr, val) {
- for (var i = 0; i < arr.length; i++) {
- if (looseEqual(arr[i], val)) { return i }
- }
- return -1
-}
-
-/* */
-
-var config = {
- /**
- * Option merge strategies (used in core/util/options)
- */
- optionMergeStrategies: Object.create(null),
-
- /**
- * Whether to suppress warnings.
- */
- silent: false,
-
- /**
- * Whether to enable devtools
- */
- devtools: "development" !== 'production',
-
- /**
- * Error handler for watcher errors
- */
- errorHandler: null,
-
- /**
- * Ignore certain custom elements
- */
- ignoredElements: null,
-
- /**
- * Custom user key aliases for v-on
- */
- keyCodes: Object.create(null),
-
- /**
- * Check if a tag is reserved so that it cannot be registered as a
- * component. This is platform-dependent and may be overwritten.
- */
- isReservedTag: no,
-
- /**
- * Check if a tag is an unknown element.
- * Platform-dependent.
- */
- isUnknownElement: no,
-
- /**
- * Get the namespace of an element
- */
- getTagNamespace: noop,
-
- /**
- * Check if an attribute must be bound using property, e.g. value
- * Platform-dependent.
- */
- mustUseProp: no,
-
- /**
- * List of asset types that a component can own.
- */
- _assetTypes: [
- 'component',
- 'directive',
- 'filter'
- ],
-
- /**
- * List of lifecycle hooks.
- */
- _lifecycleHooks: [
- 'beforeCreate',
- 'created',
- 'beforeMount',
- 'mounted',
- 'beforeUpdate',
- 'updated',
- 'beforeDestroy',
- 'destroyed',
- 'activated',
- 'deactivated'
- ],
-
- /**
- * Max circular updates allowed in a scheduler flush cycle.
- */
- _maxUpdateCount: 100,
-
- /**
- * Server rendering?
- */
- _isServer: "client" === 'server'
-};
-
-/* */
-
-/**
- * Check if a string starts with $ or _
- */
-function isReserved (str) {
- var c = (str + '').charCodeAt(0);
- return c === 0x24 || c === 0x5F
-}
-
-/**
- * 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
- }
- }
-}
-
-/* */
-/* globals MutationObserver */
-
-// can we use __proto__?
-var hasProto = '__proto__' in {};
-
-// Browser environment sniffing
-var inBrowser =
- typeof window !== 'undefined' &&
- Object.prototype.toString.call(window) !== '[object Object]';
-
-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);
-
-// detect devtools
-var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
-
-/* istanbul ignore next */
-function isNative (Ctor) {
- return /native code/.test(Ctor.toString())
-}
-
-/**
- * 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);
- };
- }
-
- return function queueNextTick (cb, ctx) {
- var func = ctx
- ? function () { cb.call(ctx); }
- : cb;
- callbacks.push(func);
- if (!pending) {
- pending = true;
- timerFunc();
- }
- }
-})();
-
-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.add = function add (key) {
- this.set[key] = 1;
- };
- Set.prototype.clear = function clear () {
- this.set = Object.create(null);
- };
-
- 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
- );
- }
- return has || !isAllowed
- }
- };
-
- initProxy = function initProxy (vm) {
- if (hasProxy) {
- vm._renderProxy = new Proxy(vm, proxyHandlers);
- } else {
- vm._renderProxy = vm;
- }
- };
-}
-
-/* */
-
-
-var uid$2 = 0;
-
-/**
- * A dep is an observable that can have multiple
- * directives subscribing to it.
- */
-var Dep = function Dep () {
- this.id = uid$2++;
- this.subs = [];
-};
-
-Dep.prototype.addSub = function addSub (sub) {
- this.subs.push(sub);
-};
-
-Dep.prototype.removeSub = function removeSub (sub) {
- remove$1(this.subs, sub);
-};
-
-Dep.prototype.depend = function depend () {
- if (Dep.target) {
- Dep.target.addDep(this);
- }
-};
-
-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();
- }
-};
-
-// 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 = [];
-
-function pushTarget (_target) {
- if (Dep.target) { targetStack.push(Dep.target); }
- Dep.target = _target;
-}
-
-function popTarget () {
- Dep.target = targetStack.pop();
-}
-
-/* */
-
-
-var queue = [];
-var has$1 = {};
-var circular = {};
-var waiting = false;
-var flushing = false;
-var index = 0;
-
-/**
- * Reset the scheduler's state.
- */
-function resetSchedulerState () {
- queue.length = 0;
- has$1 = {};
- {
- circular = {};
- }
- waiting = flushing = false;
-}
-
-/**
- * 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
- }
- }
- }
-
- // devtool hook
- /* istanbul ignore if */
- if (devtools && config.devtools) {
- devtools.emit('flush');
- }
-
- resetSchedulerState();
-}
-
-/**
- * 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 {
- // 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);
- }
- // queue the flush
- if (!waiting) {
- waiting = true;
- nextTick(flushSchedulerQueue);
- }
- }
-}
-
-/* */
-
-var uid$1 = 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.
- */
-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);
- }
- }
-};
-
-/**
- * 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);
- }
-};
-
-/**
- * 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);
- }
- }
- }
-};
-
-/**
- * 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;
-};
-
-/**
- * Depend on all deps collected by this watcher.
- */
-Watcher.prototype.depend = function depend () {
- var this$1 = this;
-
- var i = this.deps.length;
- while (i--) {
- this$1.deps[i].depend();
- }
-};
-
-/**
- * Remove self from all dependencies' subcriber list.
- */
-Watcher.prototype.teardown = function teardown () {
- var this$1 = this;
-
- 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);
- }
- var i = this.deps.length;
- while (i--) {
- this$1.deps[i].removeSub(this$1);
- }
- this.active = false;
- }
-};
-
-/**
- * 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);
- }
- }
- 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); }
- }
- }
-}
-
-/*
- * not type checking this file because flow doesn't play well with
- * dynamically accessing methods on Array prototype
- */
-
-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
- });
-});
-
-/* */
-
-var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
-
-/**
- * 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);
- }
-};
-
-/**
- * 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]]);
- }
-};
-
-/**
- * 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]);
- }
-};
-
-// helpers
-
-/**
- * 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]);
- }
-}
-
-/**
- * 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();
- }
- });
-}
-
-/**
- * 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
- );
- }
- });
- }
- };
-
- 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 {
- 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;
- }
- Object.defineProperty(vm, key, computedSharedDefinition);
- }
- }
-}
-
-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
- );
- }
- }
- }
-}
-
-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);
- }
- }
- }
-}
-
-function createWatcher (vm, key, handler) {
- var options;
- if (isPlainObject(handler)) {
- options = handler;
- handler = handler.handler;
- }
- if (typeof handler === 'string') {
- handler = vm[handler];
- }
- vm.$watch(key, handler, options);
-}
-
-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);
-
- Vue.prototype.$set = set;
- Vue.prototype.$delete = del;
-
- 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);
- }
- return function unwatchFn () {
- watcher.teardown();
- }
- };
-}
-
-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;
- }
- });
- }
-}
-
-/* */
-
-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 {
- 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 {
- 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 {
- old.fn = cur;
- on[name] = old;
- }
- }
- }
- for (name in oldOn) {
- if (!on[name]) {
- event = name.charAt(0) === '!' ? name.slice(1) : name;
- remove$$1(event, oldOn[name].invoker);
- }
- }
-}
-
-function arrInvoker (arr) {
- return function (ev) {
- var arguments$1 = arguments;
-
- var single = arguments.length === 1;
- for (var i = 0; i < arr.length; i++) {
- single ? arr[i](ev) : arr[i].apply(null, arguments$1);
- }
- }
-}
-
-function fnInvoker (o) {
- return function (ev) {
- var single = arguments.length === 1;
- single ? o.fn(ev) : o.fn.apply(null, arguments);
- }
-}
-
-/* */
-
-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 {
- // 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);
- }
- }
- }
- return res
- }
-}
-
-function createTextVNode (val) {
- return new VNode(undefined, undefined, undefined, String(val))
-}
-
-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);
- }
- }
- }
-}
-
-/* */
-
-function getFirstComponentChild (children) {
- return children && children.filter(function (c) { return c && c.componentOptions; })[0]
-}
-
-/* */
-
-var activeInstance = null;
-
-function initLifecycle (vm) {
- var options = vm.$options;
-
- // 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);
- }
-
- vm.$parent = parent;
- vm.$root = parent ? parent.$root : vm;
-
- vm.$children = [];
- vm.$refs = {};
-
- vm._watcher = null;
- vm._inactive = false;
- vm._isMounted = false;
- vm._isDestroyed = false;
- vm._isBeingDestroyed = false;
-}
-
-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 {
- warn(
- 'Failed to mount component: template or render function not defined.',
- vm
- );
- }
- }
- }
- 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');
- }
- 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);
- }
- activeInstance = prevActiveInstance;
- // update __vue__ reference
- if (prevEl) {
- prevEl.__vue__ = null;
- }
- if (vm.$el) {
- vm.$el.__vue__ = vm;
- }
- // 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;
- }
- if (vm._isMounted) {
- callHook(vm, 'updated');
- }
- };
-
- 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;
- }
- }
- // update listeners
- if (listeners) {
- var oldListeners = vm.$options._parentListeners;
- vm.$options._parentListeners = listeners;
- vm._updateListeners(listeners, oldListeners);
- }
- // resolve slots + force update if has children
- if (hasChildren) {
- vm.$slots = resolveSlots(renderChildren, vm._renderContext);
- vm.$forceUpdate();
- }
- };
-
- Vue.prototype.$forceUpdate = function () {
- var vm = this;
- if (vm._watcher) {
- vm._watcher.update();
- }
- };
-
- Vue.prototype.$destroy = function () {
- var vm = this;
- if (vm._isBeingDestroyed) {
- return
- }
- 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);
- }
- // 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);
- };
-}
-
-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);
- }
- }
- vm.$emit('hook:' + hook);
-}
-
-/* */
-
-var hooks = { init: init, prepatch: prepatch, insert: insert, destroy: destroy$1 };
-var hooksToMerge = Object.keys(hooks);
-
-function createComponent (
- Ctor,
- data,
- context,
- children,
- tag
-) {
- if (!Ctor) {
- return
- }
-
- if (isObject(Ctor)) {
- Ctor = Vue$3.extend(Ctor);
- }
-
- if (typeof Ctor !== 'function') {
- {
- warn(("Invalid Component definition: " + (String(Ctor))), context);
- }
- 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');
- }
- }
-}
-
-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 reject = function (reason) {
- "development" !== 'production' && warn(
- "Failed to resolve async component: " + (String(factory)) +
- (reason ? ("\nReason: " + reason) : '')
- );
- };
-
- 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);
- };
-
- Vue.prototype._render = function () {
- var vm = this;
- var ref = vm.$options;
- var render = ref.render;
- var staticRenderFns = ref.staticRenderFns;
- var _parentVnode = ref._parentVnode;
-
- if (vm._isMounted) {
- // clone slot nodes on re-renders
- for (var key in vm.$slots) {
- vm.$slots[key] = cloneVNodes(vm.$slots[key]);
- }
- }
-
- if (staticRenderFns && !vm._staticTrees) {
- vm._staticTrees = [];
- }
- // 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)) + ":"));
- }
- /* istanbul ignore else */
- if (config.errorHandler) {
- config.errorHandler.call(null, e, vm);
- } else {
- if (config._isServer) {
- throw e
- } else {
- setTimeout(function () { throw e }, 0);
- }
- }
- // return previous vnode to prevent render error causing blank component
- vnode = vm._vnode;
- }
- // 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
- };
-
- // 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;
- }
- }
- } else {
- tree.isStatic = true;
- tree.key = "__static__" + index;
- }
- return tree
- };
-
- // filter resolution helper
- var identity = function (_) { return _; };
- Vue.prototype._f = function resolveFilter (id) {
- return resolveAsset(this.$options, 'filters', id, true) || identity
- };
-
- // 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 ret
- };
-
- // 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
- };
-
- // 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 (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];
- }
- }
- }
- }
- return data
- };
-
- // 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 {
- defaultSlot.push(child);
- }
- }
- // ignore single whitespace
- if (defaultSlot.length && !(
- defaultSlot.length === 1 &&
- (defaultSlot[0].text === ' ' || defaultSlot[0].isComment)
- )) {
- slots.default = defaultSlot;
- }
- return slots
-}
-
-/* */
-
-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);
- }
-}
-
-function eventsMixin (Vue) {
- Vue.prototype.$on = function (event, fn) {
- var vm = this;(vm._events[event] || (vm._events[event] = [])).push(fn);
- return vm
- };
-
- Vue.prototype.$once = function (event, fn) {
- var vm = this;
- function on () {
- vm.$off(event, on);
- fn.apply(vm, arguments);
- }
- on.fn = fn;
- vm.$on(event, on);
- return vm
- };
-
- 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
- }
- }
- return vm
- };
-
- 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 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
- );
- }
- /* 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);
- };
-
- 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;
- }
- }
-
- 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;
- }
- }
- }
- return options
- }
-}
-
-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);
-}
-
-initMixin(Vue$3);
-stateMixin(Vue$3);
-eventsMixin(Vue$3);
-lifecycleMixin(Vue$3);
-renderMixin(Vue$3);
-
-var warn = noop;
-var formatComponentName;
-
-{
- var hasConsole = typeof console !== 'undefined';
-
- warn = function (msg, vm) {
- if (hasConsole && (!config.silent)) {
- console.error("[Vue warn]: " + msg + " " + (
- vm ? formatLocation(formatComponentName(vm)) : ''
- ));
- }
- };
-
- 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)) : '')
- )
- };
-
- var formatLocation = function (str) {
- if (str === 'anonymous component') {
- str += " - use the \"name\" option for better debugging messages.";
- }
- return ("\n(found in " + str + ")")
- };
-}
-
-/* */
-
-/**
- * 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;
-
-/**
- * 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)
- };
-}
-
-/**
- * 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
- }
- }
- }
-};
-
-/**
- * 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
- }
- def = components[key];
- if (isPlainObject(def)) {
- components[key] = Vue$3.extend(def);
- }
- }
- }
-}
-
-/**
- * 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.');
- }
- }
- } else if (isPlainObject(props)) {
- for (var key in props) {
- val = props[key];
- name = camelize(key);
- res[name] = isPlainObject(val)
- ? val
- : { type: val };
- }
- }
- options.props = res;
-}
-
-/**
- * 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 };
- }
- }
- }
-}
-
-/**
- * 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);
- }
- }
- function mergeField (key) {
- var strat = strats[key] || defaultStrat;
- options[key] = strat(parent[key], child[key], vm, key);
- }
- return options
-}
-
-/**
- * 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 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;
- }
- return {
- valid: valid,
- expectedType: expectedType
- }
-}
-
-/**
- * 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 isBooleanType (fn) {
- if (!Array.isArray(fn)) {
- return getType(fn) === 'Boolean'
- }
- for (var i = 0, len = fn.length; i < len; i++) {
- if (getType(fn[i]) === 'Boolean') {
- return true
- }
- }
- /* 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
- };
-}
-
-/* */
-
-function initMixin$1 (Vue) {
- Vue.mixin = function (mixin) {
- Vue.options = mergeOptions(Vue.options, mixin);
- };
-}
-
-/* */
-
-function initExtend (Vue) {
- /**
- * 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
- */
- 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;
- }
- }
- 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
- };
-}
-
-/* */
-
-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 {
- /* istanbul ignore if */
- {
- if (type === 'component' && config.isReservedTag(id)) {
- warn(
- 'Do not use built-in or reserved HTML elements as component ' +
- 'id: ' + id
- );
- }
- }
- if (type === 'component' && isPlainObject(definition)) {
- definition.name = definition.name || id;
- definition = Vue.extend(definition);
- }
- if (type === 'directive' && typeof definition === 'function') {
- definition = { bind: definition, update: definition };
- }
- this.options[type + 's'][id] = definition;
- return definition
- }
- };
- });
-}
-
-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 vnode
- },
- destroyed: function destroyed () {
- var this$1 = this;
-
- for (var key in this.cache) {
- var vnode = this$1.cache[key];
- callHook(vnode.child, 'deactivated');
- vnode.child.$destroy();
- }
- }
-};
-
-var builtInComponents = {
- KeepAlive: KeepAlive
-};
-
-/* */
-
-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;
-
- Vue.options = Object.create(null);
- config._assetTypes.forEach(function (type) {
- Vue.options[type + 's'] = Object.create(null);
- });
-
- extend(Vue.options.components, builtInComponents);
-
- initUse(Vue);
- initMixin$1(Vue);
- initExtend(Vue);
- initAssetRegisters(Vue);
-}
-
-initGlobalAPI(Vue$3);
-
-Object.defineProperty(Vue$3.prototype, '$isServer', {
- get: function () { return config._isServer; }
-});
-
-Vue$3.version = '2.0.3';
-
-/* */
-
-// attributes that should be using props for binding
-var mustUseProp = makeMap('value,selected,checked,muted');
-
-var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');
-
-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'
-);
-
-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 xlinkNS = 'http://www.w3.org/1999/xlink';
-
-var isXlink = function (name) {
- return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'
-};
-
-var getXlinkProp = function (name) {
- return isXlink(name) ? name.slice(6, name.length) : ''
-};
-
-var isFalsyAttrValue = function (val) {
- return val == null || val === false
-};
-
-/* */
-
-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);
- }
- }
- while ((parentNode = parentNode.parent)) {
- if (parentNode.data) {
- data = mergeClassData(data, parentNode.data);
- }
- }
- return genClassFromData(data)
-}
-
-function mergeClassData (child, parent) {
- return {
- staticClass: concat(child.staticClass, parent.staticClass),
- class: child.class
- ? [child.class, parent.class]
- : parent.class
- }
-}
-
-function genClassFromData (data) {
- var dynamicClass = data.class;
- var staticClass = data.staticClass;
- if (staticClass || dynamicClass) {
- return concat(staticClass, stringifyClass(dynamicClass))
- }
- /* istanbul ignore next */
- return ''
-}
-
-function concat (a, b) {
- return a ? b ? (a + ' ' + b) : a : (b || '')
-}
-
-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 + ' ';
- }
- }
- }
- return res.slice(0, -1)
- }
- if (isObject(value)) {
- for (var key in value) {
- if (value[key]) { res += key + ' '; }
- }
- 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()))
- }
-}
-
-/* */
-
-/**
- * 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;
- }
- }
-}
-
-/**
- * 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)
- *
-
-/*
- * Not type-checking this because this file is perf-critical and the cost
- * of making flow understand it is not worth it.
- */
-
-var emptyNode = new VNode('', {}, []);
-
-var hooks$1 = ['create', 'update', 'remove', 'destroy'];
-
-function isUndef (s) {
- return s == null
-}
-
-function isDef (s) {
- return s != null
-}
-
-function sameVnode (vnode1, vnode2) {
- return (
- vnode1.key === vnode2.key &&
- vnode1.tag === vnode2.tag &&
- vnode1.isComment === vnode2.isComment &&
- !vnode1.data === !vnode2.data
- )
-}
-
-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
-}
-
-function createPatchFunction (backend) {
- var i, j;
- var cbs = {};
-
- var modules = backend.modules;
- var nodeOps = backend.nodeOps;
-
- 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]]); }
- }
- }
-
- function emptyNodeAt (elm) {
- return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
- }
-
- function createRmCb (childElm, listeners) {
- function remove$$1 () {
- if (--remove$$1.listeners === 0) {
- removeElement(childElm);
- }
- }
- remove$$1.listeners = listeners;
- return remove$$1
- }
-
- function removeElement (el) {
- var parent = nodeOps.parentNode(el);
- nodeOps.removeChild(parent, el);
- }
-
- 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 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));
- }
- }
-
- function isPatchable (vnode) {
- while (vnode.child) {
- vnode = vnode.child._vnode;
- }
- return isDef(vnode.tag)
- }
-
- function invokeCreateHooks (vnode, insertedVnodeQueue) {
- for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
- cbs.create[i$1](emptyNode, vnode);
- }
- i = vnode.data.hook; // Reuse variable
- if (isDef(i)) {
- if (i.create) { i.create(emptyNode, vnode); }
- if (i.insert) { insertedVnodeQueue.push(vnode); }
- }
- }
-
- function initComponent (vnode, insertedVnodeQueue) {
- if (vnode.data.pendingInsert) {
- insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
- }
- 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);
- }
- }
-
- // 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, '');
- }
- }
-
- function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
- for (; startIdx <= endIdx; ++startIdx) {
- nodeOps.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
- }
- }
-
- 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]);
- }
- }
- }
-
- 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);
- }
- }
- }
- }
-
- 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 {
- // we have a recursively passed down rm callback
- // increase the listeners count
- rm.listeners += listeners;
- }
- // recursively invoke hooks on child component root node
- if (isDef(i = vnode.child) && isDef(i = i._vnode) && isDef(i.data)) {
- removeAndInvokeRemoveHook(i, rm);
- }
- for (i = 0; i < cbs.remove.length; ++i) {
- cbs.remove[i](vnode, rm);
- }
- if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {
- i(vnode, rm);
- } else {
- rm();
- }
- } 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 {
- 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];
- }
- }
- }
- }
- 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);
- }
- }
-
- function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
- if (oldVnode === vnode) {
- return
- }
- // 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
- }
- 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]);
- }
- }
- }
-
- var bailed = false;
- function hydrate (elm, vnode, insertedVnodeQueue) {
- {
- if (!assertNodeMatch(elm, vnode)) {
- return false
- }
- }
- 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
- }
- }
- 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 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
- }
- }
- }
- if (isDef(data)) {
- invokeCreateHooks(vnode, insertedVnodeQueue);
- }
- }
- return true
- }
-
- 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
- }
- }
-
- return function patch (oldVnode, vnode, hydrating, removeOnly) {
- if (!vnode) {
- if (oldVnode) { invokeDestroyHook(oldVnode); }
- return
- }
-
- var elm, parent;
- var isInitialPatch = false;
- var insertedVnodeQueue = [];
-
- 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 {
- 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;
- }
- 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.'
- );
- }
- }
- // 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);
- }
- }
- }
-
- if (parent !== null) {
- nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm));
- removeVnodes(parent, [oldVnode], 0, 0);
- } else if (isDef(oldVnode.tag)) {
- invokeDestroyHook(oldVnode);
- }
- }
- }
-
- invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
- return vnode.elm
- }
-}
-
-/* */
-
-var directives = {
- create: updateDirectives,
- update: updateDirectives,
- destroy: function unbindDirectives (vnode) {
- updateDirectives(vnode, emptyNode);
- }
-};
-
-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);
-
- var dirsWithInsert = [];
- var dirsWithPostpatch = [];
-
- 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);
- }
- } else {
- // existing directive, update
- dir.oldValue = oldDir.value;
- callHook$1(dir, 'update', vnode, oldVnode);
- if (dir.def && dir.def.componentUpdated) {
- dirsWithPostpatch.push(dir);
- }
- }
- }
-
- 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();
- }
- }
-
- if (dirsWithPostpatch.length) {
- mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', function () {
- dirsWithPostpatch.forEach(function (dir) {
- callHook$1(dir, 'componentUpdated', vnode, oldVnode);
- });
- }, 'dir-postpatch');
- }
-
- if (!isCreate) {
- for (key in oldDirs) {
- if (!newDirs[key]) {
- // no longer present, unbind
- callHook$1(oldDirs[key], 'unbind', oldVnode);
- }
- }
- }
-}
-
-var emptyModifiers = Object.create(null);
-
-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
-}
-
-function getRawDirName (dir) {
- return dir.rawName || ((dir.name) + "." + (Object.keys(dir.modifiers || {}).join('.')))
-}
-
-function callHook$1 (dir, hook, vnode, oldVnode) {
- var fn = dir.def && dir.def[hook];
- if (fn) {
- fn(vnode.elm, dir, vnode, oldVnode);
- }
-}
-
-var baseModules = [
- ref,
- directives
-];
-
-/* */
-
-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);
- }
-
- for (key in attrs) {
- cur = attrs[key];
- old = oldAttrs[key];
- if (old !== cur) {
- setAttr(elm, key, cur);
- }
- }
- for (key in oldAttrs) {
- if (attrs[key] == null) {
- if (isXlink(key)) {
- elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
- } else if (!isEnumeratedAttr(key)) {
- elm.removeAttribute(key);
- }
- }
- }
-}
-
-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);
- }
- } 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 {
- el.setAttributeNS(xlinkNS, key, value);
- }
- } else {
- if (isFalsyAttrValue(value)) {
- el.removeAttribute(key);
- } else {
- el.setAttribute(key, value);
- }
- }
-}
-
-var attrs = {
- create: updateAttrs,
- update: updateAttrs
-};
-
-/* */
-
-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 cls = genClassForVnode(vnode);
-
- // handle transition classes
- var transitionClass = el._transitionClasses;
- if (transitionClass) {
- cls = concat(cls, stringifyClass(transitionClass));
- }
-
- // set the class
- if (cls !== el._prevClass) {
- el.setAttribute('class', cls);
- el._prevClass = cls;
- }
-}
-
-var klass = {
- create: updateClass,
- update: updateClass
-};
-
-// skip type checking this file because we need to attach private properties
-// to elements
-
-function updateDOMListeners (oldVnode, vnode) {
- if (!oldVnode.data.on && !vnode.data.on) {
- return
- }
- 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 {
- elm[key] = cur;
- }
- }
-}
-
-var domProps = {
- create: updateDOMProps,
- update: updateDOMProps
-};
-
-/* */
-
-var prefixes = ['Webkit', 'Moz', 'ms'];
-
-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
- }
- }
-});
-
-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 || {};
-
- // handle string
- if (typeof style === 'string') {
- el.style.cssText = style;
- return
- }
-
- var needClone = style.__ob__;
-
- // handle array syntax
- if (Array.isArray(style)) {
- style = vnode.data.style = toObject(style);
- }
-
- // clone the style for future updates,
- // in case the user mutates the style object in-place.
- if (needClone) {
- style = vnode.data.style = extend({}, style);
- }
-
- for (name in oldStyle) {
- if (style[name] == null) {
- el.style[normalize(name)] = '';
- }
- }
- 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;
- }
- }
-}
-
-var style = {
- create: updateStyle,
- update: updateStyle
-};
-
-/* */
-
-/**
- * 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 {
- el.classList.add(cls);
- }
- } else {
- var cur = ' ' + el.getAttribute('class') + ' ';
- if (cur.indexOf(' ' + cls + ' ') < 0) {
- el.setAttribute('class', (cur + cls).trim());
- }
- }
-}
-
-/**
- * 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 {
- 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 hasTransition = inBrowser && !isIE9;
-var TRANSITION = 'transition';
-var ANIMATION = 'animation';
-
-// 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 onEnd = function (e) {
- if (e.target === el) {
- if (++ended >= propCount) {
- end();
- }
- }
- };
- 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;
- }
- } 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
- }
-
- /* 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;
- });
-
- 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();
- }
- enterHook && enterHook(el, cb);
- }, 'transition-insert');
- }
-
- // 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);
- }
- });
- }
-
- if (vnode.data.show) {
- enterHook && enterHook(el, cb);
- }
-
- if (!expectsCSS && !userWantsControl) {
- cb();
- }
-}
-
-function leave (vnode, rm) {
- var el = vnode.elm;
-
- // call enter callback now
- if (el._enterCb) {
- el._enterCb.cancelled = true;
- el._enterCb();
- }
-
- var data = resolveTransition(vnode.data.transition);
- if (!data) {
- return rm()
- }
-
- /* 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 {
- rm();
- afterLeave && afterLeave(el);
- }
- el._leaveCb = null;
- });
-
- if (delayLeave) {
- delayLeave(performLeave);
- } else {
- performLeave();
- }
-
- function performLeave () {
- // the delayed leave may have already been cancelled
- if (cb.cancelled) {
- return
- }
- // record leaving element
- if (!vnode.data.show) {
- (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode;
- }
- beforeLeave && beforeLeave(el);
- if (expectsCSS) {
- addTransitionClass(el, leaveClass);
- addTransitionClass(el, leaveActiveClass);
- nextFrame(function () {
- removeTransitionClass(el, leaveClass);
- if (!cb.cancelled && !userWantsControl) {
- whenTransitionEnds(el, type, cb);
- }
- });
- }
- leave && leave(el, cb);
- if (!expectsCSS && !userWantsControl) {
- cb();
- }
- }
-}
-
-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'));
- }
- extend(res, def$$1);
- return res
- } else if (typeof def$$1 === 'string') {
- return autoCssTransition(def$$1)
- }
-}
-
-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")
- }
-});
-
-function once (fn) {
- var called = false;
- return function () {
- if (!called) {
- called = true;
- fn();
- }
- }
-}
-
-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 {
- rm();
- }
- }
-} : {};
-
-var platformModules = [
- attrs,
- klass,
- events,
- domProps,
- style,
- transition
-];
-
-/* */
-
-// the directive module should be applied last, after all
-// built-in modules have been applied.
-var modules = platformModules.concat(baseModules);
-
-var patch$1 = createPatchFunction({ nodeOps: nodeOps, modules: modules });
-
-/**
- * Not type checking this file because flow doesn't like attaching
- * properties to Elements.
- */
-
-var modelableTagRE = /^input|select|textarea|vue-component-[0-9]+(-[0-9a-zA-Z_\-]*)?$/;
-
-/* 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 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
- );
- }
- }
- if (vnode.tag === 'select') {
- var cb = function () {
- setSelected(el, binding, vnode.context);
- };
- 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;
- }
-}
-
-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 getValue (option) {
- return '_value' in option
- ? option._value
- : option.value
-}
-
-function onCompositionStart (e) {
- e.target.composing = true;
-}
-
-function onCompositionEnd (e) {
- e.target.composing = false;
- trigger(e.target, 'input');
-}
-
-function trigger (el, type) {
- var e = document.createEvent('HTMLEvents');
- e.initEvent(type, true, true);
- el.dispatchEvent(e);
-}
-
-/* */
-
-// 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
-}
-
-var show = {
- bind: function bind (el, ref, vnode) {
- var value = ref.value;
-
- 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;
-
- /* 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';
- });
- }
- } 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
- }
-}
-
-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
-}
-
-function placeholder (h, rawChild) {
- return /\d-keep-alive$/.test(rawChild.tag)
- ? h('keep-alive')
- : null
-}
-
-function hasParentTransition (vnode) {
- while ((vnode = vnode.parent)) {
- if (vnode.data.transition) {
- return true
- }
- }
-}
-
-var Transition = {
- name: 'transition',
- props: transitionProps,
- abstract: true,
- render: function render (h) {
- var this$1 = this;
-
- var children = this.$slots.default;
- if (!children) {
- return
- }
-
- // filter out text nodes (possible whitespaces)
- children = children.filter(function (c) { return c.tag; });
- /* istanbul ignore if */
- if (!children.length) {
- return
- }
-
- // 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
- );
- }
-
- var mode = this.mode;
-
- // warn invalid mode
- if ("development" !== 'production' &&
- mode && mode !== 'in-out' && mode !== 'out-in') {
- warn(
- 'invalid <transition> mode: ' + mode,
- this.$parent
- );
- }
-
- var rawChild = children[0];
-
- // if this is a component root node and the component's
- // parent container node also has transition, skip.
- if (hasParentTransition(this.$vnode)) {
- return rawChild
- }
-
- // 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 + ">"));
- }
- }
- }
-
- 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);
- }
- });
- }
- });
- },
-
- methods: {
- hasMove: function hasMove (el, moveClass) {
- /* istanbul ignore if */
- if (!hasTransition) {
- return false
- }
- if (this._hasMove != null) {
- return this._hasMove
- }
- addTransitionClass(el, moveClass);
- var info = getTransitionInfo(el);
- removeTransitionClass(el, moveClass);
- return (this._hasMove = info.hasTransform)
- }
- }
-};
-
-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'
- );
- }
- }
-}, 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
-}
-
-// #3663
-// IE encodes newlines inside attribute values while other browsers don't
-var shouldDecodeNewlines = inBrowser ? shouldDecode('\n', '&#10;') : false;
-
-/* */
-
-var decoder = document.createElement('div');
-
-function decode (html) {
- decoder.innerHTML = html;
- return decoder.textContent
-}
-
-/**
- * Not type-checking this file because it's mostly vendor code.
- */
-
-/*!
- * 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
- */
-
-// 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
- }
- }
-
- // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
- if (/^<!\[/.test(html)) {
- var conditionalEnd = html.indexOf(']>');
-
- if (conditionalEnd >= 0) {
- advance(conditionalEnd + 2);
- continue
- }
- }
-
- // Doctype:
- var doctypeMatch = html.match(doctype);
- if (doctypeMatch) {
- advance(doctypeMatch[0].length);
- continue
- }
-
- // End tag:
- var endTagMatch = html.match(endTag);
- if (endTagMatch) {
- var curIndex = index;
- advance(endTagMatch[0].length);
- parseEndTag(endTagMatch[0], endTagMatch[1], curIndex, index);
- continue
- }
-
- // Start tag:
- var startTagMatch = parseStartTag();
- if (startTagMatch) {
- handleStartTag(startTagMatch);
- continue
- }
- }
-
- var text = void 0;
- if (textEnd >= 0) {
- text = html.substring(0, textEnd);
- advance(textEnd);
- } else {
- text = html;
- html = '';
- }
-
- 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);
- }
-
- if (html === last) {
- throw new Error('Error parsing template:\n\n' + html)
- }
- }
-
- // Clean up any remaining tags
- parseEndTag();
-
- function advance (n) {
- index += n;
- html = html.substring(n);
- }
-
- 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);
- }
- if (end) {
- match.unarySlash = end[1];
- advance(end[0].length);
- match.end = index;
- return match
- }
- }
- }
-
- function handleStartTag (match) {
- var tagName = match.tagName;
- var unarySlash = match.unarySlash;
-
- if (expectHTML) {
- if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
- parseEndTag('', lastTag);
- }
- if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
- parseEndTag('', tagName);
- }
- }
-
- var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;
-
- 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]; }
- }
- var value = args[3] || args[4] || args[5] || '';
- attrs[i] = {
- name: args[1],
- value: decodeAttr(
- value,
- options.shouldDecodeNewlines
- )
- };
- }
-
- if (!unary) {
- stack.push({ tag: tagName, attrs: attrs });
- lastTag = tagName;
- unarySlash = '';
- }
-
- if (options.start) {
- options.start(tagName, attrs, unary, match.start, match.end);
- }
- }
-
- function parseEndTag (tag, tagName, start, end) {
- var pos;
- if (start == null) { start = index; }
- if (end == null) { end = index; }
-
- // 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 {
- // If no tag name is provided, clean shop
- pos = 0;
- }
-
- 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);
- }
- }
-
- // 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);
- }
- } else if (tagName.toLowerCase() === 'p') {
- if (options.start) {
- options.start(tagName, [], false, start, end);
- }
- if (options.end) {
- options.end(tagName, start, end);
- }
- }
- }
-}
-
-/* */
-
-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 {
- 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 // }
- }
- }
- }
-
- if (expression === undefined) {
- expression = exp.slice(0, i).trim();
- } else if (lastFilterIndex !== 0) {
- pushFilter();
- }
-
- function pushFilter () {
- (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
- lastFilterIndex = i + 1;
- }
-
- if (filters) {
- for (i = 0; i < filters.length; i++) {
- expression = wrapFilter(expression, filters[i]);
- }
- }
-
- return expression
-}
-
-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);
- }
-
- var element = {
- type: 1,
- tag: tag,
- attrsList: attrs,
- attrsMap: makeAttrsMap(attrs, options.isIE),
- parent: currentParent,
- children: []
- };
- if (ns) {
- element.ns = ns;
- }
-
- 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 + ">."
- );
- }
-
- // apply pre-transforms
- for (var i = 0; i < preTransforms.length; i++) {
- preTransforms[i](element, options);
- }
-
- if (!inVPre) {
- processPre(element);
- if (element.pre) {
- inVPre = true;
- }
- }
- if (platformIsPreTag(element.tag)) {
- inPre = true;
- }
- 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
- );
- }
- }
- }
-
- // 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 {
- warned = true;
- warn$1(
- ("Component template should contain exactly one root element:\n\n" + template)
- );
- }
- }
- if (currentParent && !element.forbidden) {
- if (element.else) {
- processElse(element, currentParent);
- } else {
- currentParent.children.push(element);
- element.parent = currentParent;
- }
- }
- if (!unary) {
- currentParent = element;
- stack.push(element);
- }
- // apply post-transforms
- for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
- postTransforms[i$2](element, options);
- }
- },
-
- 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();
- }
- // pop stack
- stack.length -= 1;
- currentParent = stack[stack.length - 1];
- // check pre state
- if (element.pre) {
- inVPre = false;
- }
- if (platformIsPreTag(element.tag)) {
- inPre = false;
- }
- },
-
- 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
- });
- }
- }
- }
- });
- 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)
- };
- }
- } 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;
- }
- }
-}
-
-function processIf (el) {
- var exp = getAndRemoveAttr(el, 'v-if');
- if (exp) {
- el.if = exp;
- }
- if (getAndRemoveAttr(el, 'v-else') != null) {
- el.else = true;
- }
-}
-
-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.")
- );
- }
-}
-
-function processOnce (el) {
- var once = getAndRemoveAttr(el, 'v-once');
- if (once != null) {
- el.once = true;
- }
-}
-
-function processSlot (el) {
- if (el.tag === 'slot') {
- el.slotName = getBindingAttr(el, 'name');
- } else {
- var slotTarget = getBindingAttr(el, 'slot');
- if (slotTarget) {
- el.slotTarget = slotTarget;
- }
- }
-}
-
-function processComponent (el) {
- var binding;
- if ((binding = getBindingAttr(el, 'is'))) {
- el.component = binding;
- }
- if (getAndRemoveAttr(el, 'inline-template') != null) {
- el.inlineTemplate = true;
- }
-}
-
-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);
- }
- } 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));
- }
- addDirective(el, name, rawName, value, arg, modifiers);
- if ("development" !== 'production' && name === 'model') {
- checkForAliasModel(el, value);
- }
- }
- } 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.'
- );
- }
- }
- addAttr(el, name, JSON.stringify(value));
- }
- }
-}
-
-function checkInFor (el) {
- var parent = el;
- while (parent) {
- if (parent.for !== undefined) {
- return true
- }
- parent = parent.parent;
- }
- return false
-}
-
-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 {
- keys.push(key);
- }
- }
- if (keys.length) {
- code = genKeyFilter(keys) + code;
- }
- var handlerCode = simplePathRE.test(handler.value)
- ? handler.value + '($event)'
- : handler.value;
- return 'function($event){' + code + handlerCode + '}'
- }
-}
-
-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;")
- }
-}
-
-function normalizeKeyCode (key) {
- return (
- parseInt(key, 10) || // number keyCode
- keyCodes[key] || // built-in alias
- ("_k(" + (JSON.stringify(key)) + ")") // custom alias
- )
-}
-
-/* */
-
-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) + ']'
- }
-}
-
-function genChildren (el) {
- if (el.children.length) {
- return '[' + el.children.map(genNode).join(',') + ']'
- }
-}
-
-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);
- }
- }
- }
- }
- 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);
- }
-}
-
-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);
-}
-
-function checkIdentifier (ident, type, text, errors) {
- if (typeof ident === 'string' && !identRE.test(ident)) {
- errors.push(("- invalid " + type + " \"" + ident + "\" in expression: " + text));
- }
-}
-
-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 {
- 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.'
- );
- }
- }
- }
- 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)
-}
-
-function makeFunction (code) {
- try {
- return new Function(code)
- } catch (e) {
- return noop
- }
-}
-
-/* */
-
-var idToTemplate = cached(function (id) {
- var el = query(id);
- return el && el.innerHTML
-});
-
-var mount = Vue$3.prototype.$mount;
-Vue$3.prototype.$mount = function (
- el,
- hydrating
-) {
- el = el && query(el);
-
- /* 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
- }
-
- 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 {
- {
- warn('invalid template option:' + template, this);
- }
- return this
- }
- } 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;
- }
- }
- return mount.call(this, el, hydrating)
-};
-
-/**
- * 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
- }
-}
-
-Vue$3.compile = compileToFunctions;
-
-return Vue$3;
-
-})));
diff --git a/vendor/assets/javascripts/vue.js.erb b/vendor/assets/javascripts/vue.js.erb
deleted file mode 100644
index 008beb10f4d..00000000000
--- a/vendor/assets/javascripts/vue.js.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<% type = Rails.env.development? ? 'full' : 'min' %>
-<%= File.read(Rails.root.join("vendor/assets/javascripts/vue.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
deleted file mode 100644
index f86786dd454..00000000000
--- a/vendor/assets/javascripts/vue.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*!
- * Vue.js v2.0.3
- * (c) 2014-2016 Evan You
- * Released under the MIT License.
- */
-!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
diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js
index 7e24fd9b36e..55438452cad 100644
--- a/vendor/assets/javascripts/xterm/fit.js
+++ b/vendor/assets/javascripts/xterm/fit.js
@@ -16,12 +16,12 @@
/*
* CommonJS environment
*/
- module.exports = fit(require('../../xterm'));
+ module.exports = fit(require('./xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
- define(['../../xterm'], fit);
+ define(['./xterm'], fit);
} else {
/*
* Plain browser environment
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index d028d1251ad..a1a65c2d72e 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -44,3 +44,11 @@ captures/
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+#Freeline
+freeline.py
+freeline/
+freeline_project_description.json
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index 27ada0591ec..9ea395f15ee 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -1,6 +1,7 @@
CMakeCache.txt
CMakeFiles
CMakeScripts
+Testing
Makefile
cmake_install.cmake
install_manifest.txt
diff --git a/vendor/gitignore/CodeIgniter.gitignore b/vendor/gitignore/CodeIgniter.gitignore
index 60571a0c383..bfea17cdc5b 100644
--- a/vendor/gitignore/CodeIgniter.gitignore
+++ b/vendor/gitignore/CodeIgniter.gitignore
@@ -9,3 +9,9 @@ user_guide_src/build/*
user_guide_src/cilexer/build/*
user_guide_src/cilexer/dist/*
user_guide_src/cilexer/pycilexer.egg-info/*
+
+#codeigniter 3
+application/logs/*
+!application/logs/index.html
+!application/logs/.htaccess
+/vendor/
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index e375c744b6d..401fee15748 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -2,24 +2,24 @@
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
-.idea/workspace.xml
-.idea/tasks.xml
+.idea/**/workspace.xml
+.idea/**/tasks.xml
# Sensitive or high-churn files:
-.idea/dataSources/
-.idea/dataSources.ids
-.idea/dataSources.xml
-.idea/dataSources.local.xml
-.idea/sqlDataSources.xml
-.idea/dynamic.xml
-.idea/uiDesigner.xml
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
# Gradle:
-.idea/gradle.xml
-.idea/libraries
+.idea/**/gradle.xml
+.idea/**/libraries
# Mongo Explorer plugin:
-.idea/mongoSettings.xml
+.idea/**/mongoSettings.xml
## File-based project format:
*.iws
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index 32a5ad4c777..09dfde64b5f 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -17,3 +17,6 @@ slprj/
# Session info
octave-workspace
+
+# Simulink autosave extension
+.autosave
diff --git a/vendor/gitignore/Global/Stata.gitignore b/vendor/gitignore/Global/Stata.gitignore
new file mode 100644
index 00000000000..07997bb1201
--- /dev/null
+++ b/vendor/gitignore/Global/Stata.gitignore
@@ -0,0 +1,24 @@
+# .gitignore file for git projects containing Stata files
+# Commercial statistical software: http://www.stata.com
+
+# Stata dataset and output files
+*.dta
+*.gph
+*.log
+*.smcl
+*.stpr
+*.stsem
+
+# Graphic export files from Stata
+# Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf
+#
+# You may add graphic export files to your .gitignore. However you should be
+# aware that this will exclude all image files from this main directory
+# and subdirectories.
+# *.ps
+# *.eps
+# *.wmf
+# *.emf
+# *.pdf
+# *.png
+# *.tif
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index 5e1047c9d78..a1338d68517 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -1,30 +1,14 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
+# Binaries for programs and plugins
+*.exe
+*.dll
*.so
+*.dylib
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
+# Test binary, build with `go test -c`
*.test
-*.prof
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
-# External packages folder
-vendor/
+# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
+.glide/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index e44e0860405..dbb4a2dfa1a 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,5 +1,8 @@
*.class
+# Log file
+*.log
+
# BlueJ files
*.ctxt
@@ -10,6 +13,9 @@
*.jar
*.war
*.ear
+*.zip
+*.tar.gz
+*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
diff --git a/vendor/gitignore/Joomla.gitignore b/vendor/gitignore/Joomla.gitignore
index 93103fdbe77..53a74e74657 100644
--- a/vendor/gitignore/Joomla.gitignore
+++ b/vendor/gitignore/Joomla.gitignore
@@ -29,8 +29,6 @@
/administrator/components/com_search/*
/administrator/components/com_templates/*
/administrator/components/com_users/*
-/administrator/components/com_weblinks/*
-/administrator/components/index.html
/administrator/help/*
/administrator/includes/*
/administrator/language/en-GB/en-GB.com_ajax.ini
@@ -41,7 +39,6 @@
/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini
/administrator/language/en-GB/en-GB.com_postinstall.ini
/administrator/language/en-GB/en-GB.com_postinstall.sys.ini
-/administrator/language/en-GB/en-GB.com_sitemapjen.sys.ini
/administrator/language/en-GB/en-GB.com_tags.ini
/administrator/language/en-GB/en-GB.com_tags.sys.ini
/administrator/language/en-GB/en-GB.mod_stats_admin.ini
@@ -250,15 +247,10 @@
/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini
/administrator/language/en-GB/en-GB.plg_user_profile.ini
/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.ini
-/administrator/language/en-GB/en-GB.tpl_bluestork.sys.ini
/administrator/language/en-GB/en-GB.tpl_hathor.ini
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
-/administrator/language/en-GB/index.html
-/administrator/language/ru-RU/index.html
/administrator/language/overrides/*
-/administrator/language/index.html
/administrator/logs/index.html
/administrator/manifests/*
/administrator/modules/mod_custom/*
@@ -278,12 +270,9 @@
/administrator/modules/mod_unread/*
/administrator/modules/mod_version/*
/administrator/modules/mod_stats_admin/*
-/administrator/modules/index.html
-/administrator/templates/bluestork/*
/administrator/templates/isis/*
/administrator/templates/hathor/*
/administrator/templates/system/*
-/administrator/templates/index.html
/administrator/index.php
/cache/*
/bin/*
@@ -302,7 +291,6 @@
/components/com_newsfeeds/*
/components/com_search/*
/components/com_users/*
-/components/com_weblinks/*
/components/com_wrapper/*
/components/index.html
/images/banners/*
@@ -403,7 +391,6 @@
/language/en-GB/en-GB.tpl_beez5.ini
/language/en-GB/en-GB.tpl_beez5.sys.ini
/language/en-GB/en-GB.xml
-/language/en-GB/index.html
/language/en-GB/install.xml
/language/overrides/*
/language/index.html
@@ -428,8 +415,6 @@
/libraries/index.html
/libraries/import.php
/libraries/loader.php
-/libraries/platform.php
-/logs/*
/media/cms/*
/media/com_contenthistory/*
/media/com_finder/*
@@ -472,7 +457,6 @@
/modules/mod_tags_popular/*
/modules/mod_tags_similar/*
/modules/mod_users_latest/*
-/modules/mod_weblinks/*
/modules/mod_whosonline/*
/modules/mod_wrapper/*
/modules/index.html
@@ -481,9 +465,7 @@
/plugins/authentication/joomla/*
/plugins/authentication/ldap/*
/plugins/authentication/cookie/*
-/plugins/authentication/index.html
/plugins/captcha/recaptcha/*
-/plugins/captcha/index.html
/plugins/content/emailcloak/*
/plugins/content/example/*
/plugins/content/finder/*
@@ -494,27 +476,21 @@
/plugins/content/pagenavigation/*
/plugins/content/vote/*
/plugins/content/contact/*
-/plugins/content/index.html
/plugins/editors/codemirror/*
/plugins/editors/none/*
/plugins/editors/tinymce/*
-/plugins/editors/index.html
/plugins/editors-xtd/module/*
/plugins/editors-xtd/article/*
/plugins/editors-xtd/image/*
/plugins/editors-xtd/pagebreak/*
/plugins/editors-xtd/readmore/*
-/plugins/editors-xtd/index.html
/plugins/extension/example/*
/plugins/extension/joomla/*
-/plugins/extension/index.html
-/plugins/finder/index.html
/plugins/finder/categories/*
/plugins/finder/contacts/*
/plugins/finder/content/*
/plugins/finder/newsfeeds/*
/plugins/finder/tags/*
-/plugins/finder/weblinks/*
/plugins/installer/*
/plugins/quickicon/extensionupdate/*
/plugins/quickicon/joomlaupdate/*
@@ -547,10 +523,7 @@
/plugins/user/profile/*
/plugins/user/index.html
/plugins/index.html
-/templates/atomic/*
/templates/beez3/*
-/templates/beez_20/*
-/templates/beez5/*
/templates/protostar/*
/templates/system/*
/templates/index.html
diff --git a/vendor/gitignore/KiCad.gitignore b/vendor/gitignore/KiCad.gitignore
index 606ed1c7b4d..208bc4fc591 100644
--- a/vendor/gitignore/KiCad.gitignore
+++ b/vendor/gitignore/KiCad.gitignore
@@ -13,7 +13,8 @@ _autosave-*
*.net
# Autorouter files (exported from Pcbnew)
-.dsn
+*.dsn
+*.ses
# Exported BOM files
*.xml
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index a2d1564060b..a4854bef534 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,5 +1,6 @@
vendor/
node_modules/
+npm-debug.log
# Laravel 4 specific
bootstrap/compiled.php
@@ -7,10 +8,13 @@ app/storage/
# Laravel 5 & Lumen specific
public/storage
+public/hot
storage/*.key
.env.*.php
.env.php
.env
+Homestead.yaml
+Homestead.json
# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
.rocketeer/
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 195c9b7a029..b282f5cf547 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -1,104 +1,16 @@
-.htaccess.sample
-.modgit/
-.modman/
-app/code/community/Phoenix/Moneybookers/
-app/code/community/Cm/RedisSession/
-app/code/core/
-app/design/adminhtml/default/default/
-app/design/frontend/base/
-app/design/frontend/rwd/
-app/design/frontend/default/blank/
-app/design/frontend/default/default/
-app/design/frontend/default/iphone/
-app/design/frontend/default/modern/
-app/design/frontend/enterprise/default
-app/design/install/
-app/etc/modules/Enterprise_*
-app/etc/modules/Mage_*.xml
-app/etc/modules/Phoenix_Moneybookers.xml
-app/etc/modules/Cm_RedisSession.xml
-app/etc/applied.patches.list
-app/etc/config.xml
-app/etc/enterprise.xml
-app/etc/local.xml.additional
-app/etc/local.xml.template
-app/etc/local.xml
-app/.htaccess
-app/bootstrap.php
-app/locale/en_US/
-app/Mage.php
-/cron.php
-cron.sh
-dev/.htaccess
-dev/tests/functional/
-downloader/
-errors/
-favicon.ico
-/get.php
-includes/
-/index.php
-index.php.sample
-/install.php
-js/blank.html
-js/calendar/
-js/enterprise/
-js/extjs/
-js/firebug/
-js/flash/
-js/index.php
-js/jscolor/
-js/lib/
-js/mage/
-js/prototype/
-js/scriptaculous/
-js/spacer.gif
-js/tiny_mce/
-js/varien/
-lib/3Dsecure/
-lib/Apache/
-lib/flex/
-lib/googlecheckout/
-lib/.htaccess
-lib/LinLibertineFont/
-lib/Mage/
-lib/PEAR/
-lib/Pelago/
-lib/phpseclib/
-lib/Varien/
-lib/Zend/
-lib/Cm/
-lib/Credis/
-lib/Magento/
-LICENSE_AFL.txt
-LICENSE.html
-LICENSE.txt
-LICENSE_EE*
-/mage
-media/
-/api.php
-nbproject/
-pear
-pear/
-php.ini.sample
-pkginfo/
-RELEASE_NOTES.txt
-shell/.htaccess
-shell/abstract.php
-shell/compiler.php
-shell/indexer.php
-shell/log.php
-sitemap.xml
-skin/adminhtml/default/default/
-skin/adminhtml/default/enterprise
-skin/frontend/base/
-skin/frontend/rwd/
-skin/frontend/default/blank/
-skin/frontend/default/blue/
-skin/frontend/default/default/
-skin/frontend/default/french/
-skin/frontend/default/german/
-skin/frontend/default/iphone/
-skin/frontend/default/modern/
-skin/frontend/enterprise
-skin/install/
-var/
+#--------------------------#
+# Magento Default Files #
+#--------------------------#
+
+/app/etc/local.xml
+/media/*
+!/media/.htaccess
+!/media/customer/.htaccess
+!/media/dhl/logo.jpg
+!/media/downloadable/.htaccess
+!/media/xmlconnect/custom/ok.gif
+!/media/xmlconnect/original/ok.gif
+!/media/xmlconnect/system/ok.gif
+/var/*
+!/var/.htaccess
+!/var/package/*.xml
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 9a439fcd988..38ac77e405e 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -21,6 +21,9 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
+# Bower dependency directory (https://bower.io/)
+bower_components
+
# node-waf configuration
.lock-wscript
@@ -28,8 +31,11 @@ coverage
build/Release
# Dependency directories
-node_modules
-jspm_packages
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
# Optional npm cache directory
.npm
@@ -46,3 +52,6 @@ jspm_packages
# Yarn Integrity file
.yarn-integrity
+# dotenv environment variables file
+.env
+
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 58c51ecaed4..af90c007a3f 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
diff --git a/vendor/gitignore/Perl.gitignore b/vendor/gitignore/Perl.gitignore
index d41364ab18e..9bf1537f6ae 100644
--- a/vendor/gitignore/Perl.gitignore
+++ b/vendor/gitignore/Perl.gitignore
@@ -4,6 +4,7 @@
/META.json
/MYMETA.*
*.o
+*.pm.tdy
*.bs
# Devel::Cover
diff --git a/vendor/gitignore/PureScript.gitignore b/vendor/gitignore/PureScript.gitignore
new file mode 100644
index 00000000000..361cf5277ba
--- /dev/null
+++ b/vendor/gitignore/PureScript.gitignore
@@ -0,0 +1,8 @@
+# Dependencies
+.psci_modules
+bower_components
+node_modules
+
+# Generated files
+.psci
+output
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 9a05e2debe5..cf3102d6b00 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -80,7 +80,7 @@ celerybeat-schedule
.env
# virtualenv
-.venv/
+.venv
venv/
ENV/
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index a02d882cb88..006a7b247fe 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -13,6 +13,8 @@ project/boot/
project/plugins/project/
# Scala-IDE specific
+.ensime
+.ensime_cache/
.scala_dependencies
.worksheet
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 2c22487b5e3..099d22ae2f4 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -19,7 +19,8 @@ xcuserdata/
## Other
*.moved-aside
-*.xcuserstate
+*.xccheckout
+*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
@@ -35,6 +36,7 @@ playground.xcworkspace
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
+# Package.pins
.build/
# CocoaPods
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 1c10388911b..b829399ae85 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,6 +5,9 @@
/[Bb]uilds/
/Assets/AssetStoreTools*
+# Visual Studio 2015 cache directory
+/.vs/
+
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
@@ -18,6 +21,7 @@ ExportedObj/
*.pidb
*.booproj
*.svd
+*.pdb
# Unity3D generated meta files
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index beec7b91f15..2f096001fec 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -36,6 +36,7 @@
# These project files can be generated by the engine
*.xcodeproj
+*.xcworkspace
*.sln
*.suo
*.opensdf
@@ -56,6 +57,9 @@ Build/*
# Don't ignore icon files in Build
!Build/**/*.ico
+# Built data for maps
+*_BuiltData.uasset
+
# Configuration files generated by the Editor
Saved/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index d9e876cfcdd..8054980d742 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -199,7 +199,6 @@ ClientBin/
*.jfm
*.pfx
*.publishsettings
-node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
@@ -234,6 +233,10 @@ FakesAssemblies/
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
+node_modules/
+
+# Typescript v1 declaration files
+typings/
# Visual Studio 6 build log
*.plg
@@ -271,4 +274,5 @@ __pycache__/
*.pyc
# Cake - Uncomment if you are using it
-# tools/
+# tools/**
+# !tools/packages.config
diff --git a/vendor/gitignore/Waf.gitignore b/vendor/gitignore/Waf.gitignore
index 48e8d8f7be4..dad2b56bdda 100644
--- a/vendor/gitignore/Waf.gitignore
+++ b/vendor/gitignore/Waf.gitignore
@@ -1,4 +1,9 @@
-# for projects that use Waf for building: http://code.google.com/p/waf/
-.waf-*
-.waf3-*
-.lock-*
+# For projects that use the Waf build system: https://waf.io/
+# Dot-hidden on Unix-like systems
+.waf-*-*/
+.waf3-*-*/
+# Hidden directory on Windows (no dot)
+waf-*-*/
+waf3-*-*/
+# Lockfile
+.lock-waf_*_build
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index 36dfc539b3b..7298ea73bab 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -1,4 +1,4 @@
-# Explaination on the scripts:
+# Explanation on the scripts:
# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md
image: registry.gitlab.com/gitlab-examples/kubernetes-deploy